首页 文章 精选 留言 我的

精选列表

搜索[fota升级],共10000篇文章
优秀的个人博客,低调大师

openGauss 存储功能全面升级,支持 PACKAGE、PLDEBUGGER 等诸多新特性

存储过程类似于面向过程语言当中的函数,可以实现面向过程语言当中的声明变量、逻辑判断、条件循环等操作,是一组完成特定功能的SQL语句集合。 在 openGauss 2.1 之前的版本中,存储过程支持了定义变量、条件循环、逻辑判断等基本功能,但是没有面向对象语言中类的形式,因此无法对存储过程中的变量以及函数进行封装,也无法在存储过程内使用全局变量。在以前的版本中没有支持类似gdb的功能,用户调试存储过程只能够使用raise info等打印变量的方式。并且在之前的版本当中,存储过程发生异常后,没有自治事务,记录错误日志不便。 而在新的版本当中,通过支持 PACKAGE、存储过程调试/PLDEBUGGER、自治事务等新功能,解决了上述问题。下面将介绍新特性的应用场景以及使用方法。 特性一:PACKAGE package是一组相关存储过程、函数、变量、常量、游标等PL/SQL程序的组合,具有面向对象的特点,可以对PL/SQL程序设计元素进行封装。package中的函数具有统一性,创建、删除、修改都统一进行。 package包含包头(Package Specification)和Package Body两个部分,其中包头所包含的声明可以被外部函数、匿名块等访问,而在包体中包含的声明不能被外部函数、匿名块等访问,只能被包体内函数和存储过程等访问。 下面可以看一组简单的例子理解一下: CREATE TABLE tab1(col1 int); CREATE OR REPLACE PACKAGE PCK1 --包头,在包头内声明的变量存储过程等都为公有的,可以被外部访问 IS public_var1 int:=1; --在包头声明的公有变量public_var1,可以被外部访问 procedure public_proc1(col1 int,col2 int); --只在包头内声明的存储过程,因此为公有的,可以被外部访问。 END PCK1; / CREATE OR REPLACE PACKAGE BODY PCK1 --指定了PACKAGE BODY关键字 IS private_var1 int:=1; --在包体内声明的私有变量private_var1,不能被外部访问 procedure private_proc1(col1 int,col2 int) --只在包体内定义的存储过程为私有存储过程 is begin raise notice 'col1 + col2 = %',col1+col2; insert into tab1 values(col1+col2); end; procedure public_proc1(col1 int,col2 int)--只在包体内定义的存储过程为私有存储过程 is begin private_proc1(1,2); end; --需与包头保持一致 END PCK1; / > --package中的函数调用方式与存储过程调用方式一样 > call pck1.public_proc1(1,2); 特性二、PLDEBUGGER DBE_PLDEBUGGER用于调试存储过程,类似于GDB的功能,可以使用单步调试,设置断点、打印调用堆栈等功能,方便了存储过程的调试,减小了存储过程的开发难度。详细的使用方法以及说明可以参考在本文末提供的[PLDEBUGGER接口及示例] 特性三、自治事务 自治事务(Autonomous Transaction),在主事务执行过程中新启的独立的事务。自治事务的提交和回滚不会影响主事务已提交的数据,同时自治事务也不受主事务影响。 自治事务在存储过程、函数和匿名块中定义,用PRAGMA AUTONOMOUS_TRANSACTION关键字来声明。 自治事务一般用于存储过程发生异常后,处理日志的时候。 下面可以看一个简单的示例: create table t2(a int, b int); insert into t2 values(1,2); select * from t2; --创建包含自治事务的存储过程 CREATE OR REPLACE PROCEDURE autonomous_proc1(a int, b int) AS DECLARE num3 int := a; num4 int := b; PRAGMA AUTONOMOUS_TRANSACTION; --声明此存储过程为一个自治事务的存储过程 BEGIN insert into t2 values(num3, num4); dbe_output.print_line('just use call.'); END; / --创建调用自治事务存储过程的普通存储过程 CREATE OR REPLACE PROCEDURE proc1(a int, b int) AS DECLARE c int:=0; BEGIN dbe_output.print_line('just no use call.'); insert into t2 values(5, 6);--异常后回滚 c:=c/0; exception when others then autonomous_proc1(a,b); END; / --调用普通存储过程 select proc1(11,22); select * from t2 order by a; 输出结果: a | b ----+---- 1 | 2 11 | 22 (2 rows) pldebugger 接口及示例 接口名称 描述 DBE_PLDEBUGGER.turn_on server端调用,标记存储过程可以调试,调用后执行该存储过程时会hang住等待调试信息。 DBE_PLDEBUGGER.turn_off server端调用,标记存储过程关闭调试。 DBE_PLDEBUGGER.local_ debug_server_info server端调用,打印本session内所有已turn_on的存储过程。 DBE_PLDEBUGGER.attach debug端调用,关联到正在调试存储过程。 DBE_PLDEBUGGER.info_locals debug端调用,打印正在调试的存储过程中的变量当前值。 DBE_PLDEBUGGER.next debug端调用,单步执行。 DBE_PLDEBUGGER.continue debug端调用,继续执行,直到断点或存储过程结束。 DBE_PLDEBUGGER.abort debug端调用,停止调试,server端报错长跳转。 DBE_PLDEBUGGER.print_var debug端调用,打印正在调试的存储过程中指定的变量当前值。 DBE_PLDEBUGGER.info_code debug和server端都可以调用,打印指定存储过程的源语句和各行对应的行号。 DBE_PLDEBUGGER.step debug端调用,单步进入执行。 DBE_PLDEBUGGER.add_ breakpoint debug端调用,新增断点。 DBE_PLDEBUGGER.delete_ breakpoint debug端调用,删除断点。 DBE_PLDEBUGGER.info_ breakpoint debug端调用,查看当前的所有断点。 DBE_PLDEBUGGER.backtrace debug端调用,查看当前的调用栈。 DBE_PLDEBUGGER.enable_ breakpoint debug端调用,激活被禁用的断点。 DBE_PLDEBUGGER.disable_ breakpoint debug端调用,禁用已激活的断点 DBE_PLDEBUGGER.finish debug端调用,继续调试,直到断点或返回上一层调用栈。 DBE_PLDEBUGGER.set_var debug端调用,为变量进行赋值操作。 示例 准备调试 通过PG_PROC,查找到待调试存储过程的oid,并执行DBE_PLDEBUGGER.turn_on(oid)。本客户端就会作为server端使用 CREATE OR REPLACE PROCEDURE test_debug ( IN x INT) AS BEGIN INSERT INTO t1 (a) VALUES (x); DELETE FROM t1 WHERE a = x; END; / 输出结果: CREATE PROCEDURE SELECT OID FROM PG_PROC WHERE PRONAME='test_debug'; 输出结果: oid ------- 16389 (1 row) SELECT * FROM DBE_PLDEBUGGER.turn_on(16389); 输出结果: nodename | port ----------+------ datanode | 0 (1 row) 开始调试 server端执行存储过程,会在存储过程内第一条SQL语句前hang住,等待debug端发送的调试消息。仅支持直接执行存储过程的调试,不支持通过trigger调用执行的存储过程调试。 call test_debug(1); 再起一个客户端,作为debug端,通过turn_on返回的数据,调用DBE_PLDEBUGGER.attach关联到该存储过程上进行调试。 SELECT * FROM DBE_PLDEBUGGER.attach('datanode',0); 输出结果: funcoid | funcname | lineno | query ---------+------------+--------+---------------------------------- 16389 | test_debug | 3 | INSERT INTO t1 (a) VALUES (x); (1 row) 在执行 attach 的客户端调试,执行下一条 statement。 SELECT * FROM DBE_PLDEBUGGER.next(); 输出结果: funcoid | funcname | lineno | query ---------+------------+--------+---------------------- 16389 | test_debug | 0 | [EXECUTION FINISHED] (1 row) 在执行 attach 的客户端调试,可以执行以下变量操作 SELECT * FROM DBE_PLDEBUGGER.info_locals(); --打印全部变量 输出结果: varname | vartype | value | package_name | isconst ---------+---------+-------+--------------+--------- x | int4 | 1 | | f (1 row) SELECT * FROM DBE_PLDEBUGGER.set_var('x', 2); --变量赋值 输出结果: set_var --------- t (1 row) SELECT * FROM DBE_PLDEBUGGER.print_var('x'); --打印单个变量 输出结果: varname | vartype | value | package_name | isconst ---------+---------+-------+--------------+--------- x | int4 | 2 | | f (1 row) 直接执行完成当前正在调试的存储过程。 SELECT * FROM DBE_PLDEBUGGER.continue(); 输出结果: funcoid | funcname | lineno | query ---------+------------+--------+---------------------- 16389 | test_debug | 0 | [EXECUTION FINISHED] (1 row) 直接退出当前正在调试的存储过程,不执行尚未执行的语句。 SELECT * FROM DBE_PLDEBUGGER.abort(); 输出结果: abort ------- t (1 row) client端查看代码信息并识别可以设置断点行号。 SELECT * FROM DBE_PLDEBUGGER.info_code(16389); 输出结果: lineno | query | canbreak --------+-----------------------------------------------------------+---------- | CREATE OR REPLACE PROCEDURE public.test_debug( IN x INT) | f 1 | AS DECLARE | f 2 | BEGIN | f 3 | INSERT INTO t1 (a) VALUES (x); | t 4 | DELETE FROM t1 WHERE a = x; | t 5 | END; | f 6 | / | f (7 rows) 设置断点 SELECT * FROM DBE_PLDEBUGGER.info_breakpoints(); 输出结果: breakpointno | funcoid | lineno | query | enable --------------+---------+--------+---------------------------------+-------- 0 | 16389 | 4 | DELETE FROM t1 WHERE a = x; | t 查看断点信息 SELECT * FROM DBE_PLDEBUGGER.info_breakpoints(); 输出结果: breakpointno | funcoid | lineno | query | enable --------------+---------+--------+---------------------------------+-------- 0 | 16389 | 4 | DELETE FROM t1 WHERE a = x; | t (1 row) 执行至断点 SELECT * FROM DBE_PLDEBUGGER.continue(); 输出结果: funcoid | funcname | lineno | query ---------+------------+--------+--------------------------------- 16389 | test_debug | 4 | DELETE FROM t1 WHERE a = x;

优秀的个人博客,低调大师

Spring Cloud 升级之路 - 2020.0.x - 7. 使用 Spring Cloud

本项目代码地址:https://github.com/HashZhang/spring-cloud-scaffold/tree/master/spring-cloud-iiford 我们使用 Spring Cloud 官方推荐的 Spring Cloud LoadBalancer 作为我们的客户端负载均衡器。上一节我们了解了 Spring Cloud LoadBalancer 的结构,接下来我们来说一下我们在使用 Spring Cloud LoadBalancer 要实现的功能: 我们要实现不同集群之间不互相调用,通过实例的metamap中的zone配置,来区分不同集群的实例。只有实例的metamap中的zone配置一样的实例才能互相调用。这个通过实现自定义的 ServiceInstanceListSupplier 即可实现 负载均衡的轮询算法,需要请求与请求之间隔离,不能共用同一个 position 导致某个请求失败之后的重试还是原来失败的实例。上一节看到的默认的 RoundRobinLoadBalancer 是所有线程共用同一个原子变量 position 每次请求原子加 1。在这种情况下会有问题:假设有微服务 A 有两个实例:实例 1 和实例 2。请求 A 到达时,RoundRobinLoadBalancer 返回实例 1,这时有请求 B 到达,RoundRobinLoadBalancer 返回实例 2。然后如果请求 A 失败重试,RoundRobinLoadBalancer 又返回了实例 1。这不是我们期望看到的。 针对这两个功能,我们分别编写自己的实现。 实现不同集群不互相调用 Spring Cloud LoadBalancer 中的 zone 配置 Spring Cloud LoadBalancer 定义了 LoadBalancerZoneConfig: public class LoadBalancerZoneConfig { //标识当前负载均衡器处于哪一个 zone private String zone; public LoadBalancerZoneConfig(String zone) { this.zone = zone; } public String getZone() { return zone; } public void setZone(String zone) { this.zone = zone; } } 如果没有引入 Eureka 相关依赖,则这个 zone 通过 spring.cloud.loadbalancer.zone 配置:LoadBalancerAutoConfiguration @Bean @ConditionalOnMissingBean public LoadBalancerZoneConfig zoneConfig(Environment environment) { return new LoadBalancerZoneConfig(environment.getProperty("spring.cloud.loadbalancer.zone")); } 如果引入了 Eureka 相关依赖,则如果在 Eureka 元数据配置了 zone,则这个 zone 会覆盖 Spring Cloud LoadBalancer 中的 LoadBalancerZoneConfig: EurekaLoadBalancerClientConfiguration @PostConstruct public void postprocess() { if (!StringUtils.isEmpty(zoneConfig.getZone())) { return; } String zone = getZoneFromEureka(); if (!StringUtils.isEmpty(zone)) { if (LOG.isDebugEnabled()) { LOG.debug("Setting the value of '" + LOADBALANCER_ZONE + "' to " + zone); } //设置 `LoadBalancerZoneConfig` zoneConfig.setZone(zone); } } private String getZoneFromEureka() { String zone; //是否配置了 spring.cloud.loadbalancer.eureka.approximateZoneFromHostname 为 true boolean approximateZoneFromHostname = eurekaLoadBalancerProperties.isApproximateZoneFromHostname(); //如果配置了,则尝试从 Eureka 配置的 host 名称中提取 //实际就是以 . 分割 host,然后第二个就是 zone //例如 www.zone1.com 就是 zone1 if (approximateZoneFromHostname && eurekaConfig != null) { return ZoneUtils.extractApproximateZone(this.eurekaConfig.getHostName(false)); } else { //否则,从 metadata map 中取 zone 这个 key zone = eurekaConfig == null ? null : eurekaConfig.getMetadataMap().get("zone"); //如果这个 key 不存在,则从配置中以 region 从 zone 列表取第一个 zone 作为当前 zone if (StringUtils.isEmpty(zone) && clientConfig != null) { String[] zones = clientConfig.getAvailabilityZones(clientConfig.getRegion()); // Pick the first one from the regions we want to connect to zone = zones != null && zones.length > 0 ? zones[0] : null; } return zone; } } 实现 SameZoneOnlyServiceInstanceListSupplier 为了实现通过 zone 来过滤同一 zone 下的实例,并且绝对不会返回非同一 zone 下的实例,我们来编写代码: SameZoneOnlyServiceInstanceListSupplier /** * 只返回与当前实例同一个 Zone 的服务实例,不同 zone 之间的服务不互相调用 */ public class SameZoneOnlyServiceInstanceListSupplier extends DelegatingServiceInstanceListSupplier { /** * 实例元数据 map 中表示 zone 配置的 key */ private final String ZONE = "zone"; /** * 当前 spring cloud loadbalancer 的 zone 配置 */ private final LoadBalancerZoneConfig zoneConfig; private String zone; public SameZoneOnlyServiceInstanceListSupplier(ServiceInstanceListSupplier delegate, LoadBalancerZoneConfig zoneConfig) { super(delegate); this.zoneConfig = zoneConfig; } @Override public Flux<List<ServiceInstance>> get() { return getDelegate().get().map(this::filteredByZone); } //通过 zoneConfig 过滤 private List<ServiceInstance> filteredByZone(List<ServiceInstance> serviceInstances) { if (zone == null) { zone = zoneConfig.getZone(); } if (zone != null) { List<ServiceInstance> filteredInstances = new ArrayList<>(); for (ServiceInstance serviceInstance : serviceInstances) { String instanceZone = getZone(serviceInstance); if (zone.equalsIgnoreCase(instanceZone)) { filteredInstances.add(serviceInstance); } } if (filteredInstances.size() > 0) { return filteredInstances; } } /** * @see ZonePreferenceServiceInstanceListSupplier 在没有相同zone实例的时候返回的是所有实例 * 我们这里为了实现不同 zone 之间不互相调用需要返回空列表 */ return List.of(); } //读取实例的 zone,没有配置则为 null private String getZone(ServiceInstance serviceInstance) { Map<String, String> metadata = serviceInstance.getMetadata(); if (metadata != null) { return metadata.get(ZONE); } return null; } } 实现请求与请求之间隔离的负载均衡算法 在之前章节的讲述中,我们提到了我们使用 spring-cloud-sleuth 作为链路追踪库。我们想可以通过其中的 traceId,来区分究竟是否是同一个请求。 RoundRobinWithRequestSeparatedPositionLoadBalancer //一定必须是实现ReactorServiceInstanceLoadBalancer //而不是ReactorLoadBalancer<ServiceInstance> //因为注册的时候是ReactorServiceInstanceLoadBalancer @Log4j2 public class RoundRobinWithRequestSeparatedPositionLoadBalancer implements ReactorServiceInstanceLoadBalancer { private final ServiceInstanceListSupplier serviceInstanceListSupplier; //每次请求算上重试不会超过1分钟 //对于超过1分钟的,这种请求肯定比较重,不应该重试 private final LoadingCache<Long, AtomicInteger> positionCache = Caffeine.newBuilder().expireAfterWrite(1, TimeUnit.MINUTES) //随机初始值,防止每次都是从第一个开始调用 .build(k -> new AtomicInteger(ThreadLocalRandom.current().nextInt(0, 1000))); private final String serviceId; private final Tracer tracer; public RoundRobinWithRequestSeparatedPositionLoadBalancer(ServiceInstanceListSupplier serviceInstanceListSupplier, String serviceId, Tracer tracer) { this.serviceInstanceListSupplier = serviceInstanceListSupplier; this.serviceId = serviceId; this.tracer = tracer; } @Override public Mono<Response<ServiceInstance>> choose(Request request) { return serviceInstanceListSupplier.get().next().map(serviceInstances -> getInstanceResponse(serviceInstances)); } private Response<ServiceInstance> getInstanceResponse(List<ServiceInstance> serviceInstances) { if (serviceInstances.isEmpty()) { log.warn("No servers available for service: " + this.serviceId); return new EmptyResponse(); } return getInstanceResponseByRoundRobin(serviceInstances); } private Response<ServiceInstance> getInstanceResponseByRoundRobin(List<ServiceInstance> serviceInstances) { if (serviceInstances.isEmpty()) { log.warn("No servers available for service: " + this.serviceId); return new EmptyResponse(); } //为了解决原始算法不同调用并发可能导致一个请求重试相同的实例 Span currentSpan = tracer.currentSpan(); if (currentSpan == null) { currentSpan = tracer.newTrace(); } long l = currentSpan.context().traceId(); AtomicInteger seed = positionCache.get(l); int s = seed.getAndIncrement(); int pos = s % serviceInstances.size(); log.info("position {}, seed: {}, instances count: {}", pos, s, serviceInstances.size()); return new DefaultResponse(serviceInstances.stream() //实例返回列表顺序可能不同,为了保持一致,先排序再取 .sorted(Comparator.comparing(ServiceInstance::getInstanceId)) .collect(Collectors.toList()).get(pos)); } } 将上述两个元素加入我们自定义的 LoadBalancerClient 并启用 在上一节,我们提到了可以通过 @LoadBalancerClients 注解配置默认的负载均衡器配置,我们这里就是通过这种方式进行配置。首先在 spring.factories 中添加自动配置类: spring.factories org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.github.hashjang.spring.cloud.iiford.service.common.auto.LoadBalancerAutoConfiguration 然后编写这个自动配置类,其实很简单,就是添加一个 @LoadBalancerClients 注解,设置默认配置类: LoadBalancerAutoConfiguration @Configuration(proxyBeanMethods = false) @LoadBalancerClients(defaultConfiguration = DefaultLoadBalancerConfiguration.class) public class LoadBalancerAutoConfiguration { } 编写这个默认配置类,将上面我们实现的两个类,组装进去: DefaultLoadBalancerConfiguration @Configuration(proxyBeanMethods = false) public class DefaultLoadBalancerConfiguration { @Bean public ServiceInstanceListSupplier serviceInstanceListSupplier( DiscoveryClient discoveryClient, Environment env, ConfigurableApplicationContext context, LoadBalancerZoneConfig zoneConfig ) { ObjectProvider<LoadBalancerCacheManager> cacheManagerProvider = context .getBeanProvider(LoadBalancerCacheManager.class); return //开启服务实例缓存 new CachingServiceInstanceListSupplier( //只能返回同一个 zone 的服务实例 new SameZoneOnlyServiceInstanceListSupplier( //启用通过 discoveryClient 的服务发现 new DiscoveryClientServiceInstanceListSupplier( discoveryClient, env ), zoneConfig ) , cacheManagerProvider.getIfAvailable() ); } @Bean public ReactorLoadBalancer<ServiceInstance> reactorServiceInstanceLoadBalancer( Environment environment, ServiceInstanceListSupplier serviceInstanceListSupplier, Tracer tracer ) { String name = environment.getProperty(LoadBalancerClientFactory.PROPERTY_NAME); return new RoundRobinWithRequestSeparatedPositionLoadBalancer( serviceInstanceListSupplier, name, tracer ); } } 这样,我们就实现了自定义的负载均衡器。也理解了 Spring Cloud LoadBalancer 的使用。接下来,我们来单元测试下这些功能。集成测试后面会有单独的章节,不用着急。 单元测试上述功能 通过这届单元测试,我们也可以了解下一般我们实现 spring cloud 自定义的基础组件,怎么去单元测试。 这里的单元测试主要测试三个场景: 只返回同一个 zone 下的实例,其他 zone 的不会返回 对于多个请求,每个请求返回的与上次的实例不同。 对于多线程的每个请求,如果重试,返回的都是不同的实例 编写代码:LoadBalancerTest //SpringRunner也包含了MockitoJUnitRunner,所以 @Mock 等注解也生效了 @RunWith(SpringRunner.class) @SpringBootTest(properties = {LoadBalancerEurekaAutoConfiguration.LOADBALANCER_ZONE + "=zone1"}) public class LoadBalancerTest { @EnableAutoConfiguration(exclude = EurekaDiscoveryClientConfiguration.class) @Configuration public static class App { @Bean public DiscoveryClient discoveryClient() { ServiceInstance zone1Instance1 = Mockito.mock(ServiceInstance.class); ServiceInstance zone1Instance2 = Mockito.mock(ServiceInstance.class); ServiceInstance zone2Instance3 = Mockito.mock(ServiceInstance.class); Map<String, String> zone1 = Map.ofEntries( Map.entry("zone", "zone1") ); Map<String, String> zone2 = Map.ofEntries( Map.entry("zone", "zone2") ); when(zone1Instance1.getMetadata()).thenReturn(zone1); when(zone1Instance1.getInstanceId()).thenReturn("instance1"); when(zone1Instance2.getMetadata()).thenReturn(zone1); when(zone1Instance2.getInstanceId()).thenReturn("instance2"); when(zone2Instance3.getMetadata()).thenReturn(zone2); when(zone2Instance3.getInstanceId()).thenReturn("instance3"); DiscoveryClient mock = Mockito.mock(DiscoveryClient.class); Mockito.when(mock.getInstances("testService")) .thenReturn(List.of(zone1Instance1, zone1Instance2, zone2Instance3)); return mock; } } @Autowired private LoadBalancerClientFactory loadBalancerClientFactory; @Autowired private Tracer tracer; /** * 只返回同一个 zone 下的实例 */ @Test public void testFilteredByZone() { ReactiveLoadBalancer<ServiceInstance> testService = loadBalancerClientFactory.getInstance("testService"); for (int i = 0; i < 100; i++) { ServiceInstance server = Mono.from(testService.choose()).block().getServer(); //必须处于和当前实例同一个zone下 Assert.assertEquals(server.getMetadata().get("zone"), "zone1"); } } /** * 返回不同的实例 */ @Test public void testReturnNext() { ReactiveLoadBalancer<ServiceInstance> testService = loadBalancerClientFactory.getInstance("testService"); //获取服务实例 ServiceInstance server1 = Mono.from(testService.choose()).block().getServer(); ServiceInstance server2 = Mono.from(testService.choose()).block().getServer(); //每次选择的是不同实例 Assert.assertNotEquals(server1.getInstanceId(), server2.getInstanceId()); } /** * 跨线程,默认情况下是可能返回同一实例的,在我们的实现下,保持 * span 则会返回下一个实例,这样保证多线程环境同一个 request 重试会返回下一实例 * @throws Exception */ @Test public void testSameSpanReturnNext() throws Exception { Span span = tracer.nextSpan(); //测试 100 次 for (int i = 0; i < 100; i++) { try (Tracer.SpanInScope cleared = tracer.withSpanInScope(span)) { ReactiveLoadBalancer<ServiceInstance> testService = loadBalancerClientFactory.getInstance("testService"); //获取实例 ServiceInstance server1 = Mono.from(testService.choose()).block().getServer(); AtomicReference<ServiceInstance> server2 = new AtomicReference<>(); Thread thread = new Thread(() -> { //保持 trace,这样就会认为仍然是同一个请求上下文,这样模拟重试 try (Tracer.SpanInScope cleared2 = tracer.withSpanInScope(span)) { server2.set(Mono.from(testService.choose()).block().getServer()); } }); thread.start(); thread.join(); System.out.println(i); Assert.assertNotEquals(server1.getInstanceId(), server2.get().getInstanceId()); } } } } 运行测试,测试通过。 微信搜索“我的编程喵”关注公众号,加作者微信,每日一刷,轻松提升技术,斩获各种offer:

优秀的个人博客,低调大师

Spring Cloud 升级之路 - 2020.0.x - 3. Undertow 的 acces

上一节我们讲述了如何使用 Undertow 作为我们的 Web 服务容器,本小节我们来分析使用 Undertow 的另一个问题,也就是如何配置 accesslog,以及 accesslog 的各种占位符。 # accesslog 相关配置 ``` server: undertow: # access log相关配置 accesslog: # 存放目录,默认为 logs dir: ./log # 是否开启 enabled: true # 格式,各种占位符后面会详细说明 pattern: '{ "transportProtocol":"%{TRANSPORT_PROTOCOL}", "scheme":"%{SCHEME}", "protocol":"%{PROTOCOL}", "method":"%{METHOD}", "reqHeaderUserAgent":"%{i,User-Agent}", "cookieUserId": "%{c,userId}", "queryTest": "%{q,test}", "queryString": "%q", "relativePath": "%R, %{REQUEST_PATH}, %{RESOLVED_PATH}", "requestLine": "%r", "uri": "%U", "thread": "%I", "hostPort": "%{HOST_AND_PORT}", "localIp": "%A", "localPort": "%p", "localServerName": "%v", "remoteIp": "%a", "remoteHost": "%h", "bytesSent": "%b", "time":"%{time,yyyy-MM-dd HH:mm:ss.S}", "status":"%s", "reason":"%{RESPONSE_REASON_PHRASE}", "respHeaderUserSession":"%{o,userSession}", "respCookieUserId":"%{resp-cookie,userId}", "timeUsed":"%Dms, %Ts, %{RESPONSE_TIME}ms, %{RESPONSE_TIME_MICROS} us, %{RESPONSE_TIME_NANOS} ns", }' # 文件前缀,默认为 access_log prefix: access. # 文件后缀,默认为 log suffix: log # 是否另起日志文件写 access log,默认为 true # 目前只能按照日期进行 rotate,一天一个日志文件 rotate: true ``` ## 注意点 1:日志文件 rotate 目前只能按照日期 Undertow 的 accesslog 处理核心类抽象是 `io.undertow.server.handlers.accesslog.AccesslogReceiver`。由于目前 Undertow 的 `AccesslogReceiver` 只有一种实现在使用,也就是 `io.undertow.server.handlers.accesslog.DefaultAccessLogReceiver`。 查看 `DefaultAccessLogReceiver` 的 rotate 时机: [`DefaultAccessLogReceiver`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/server/handlers/accesslog/DefaultAccessLogReceiver.java) ``` /** * 计算 rotate 时间点 */ private void calculateChangeOverPoint() { Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.SECOND, 0); calendar.set(Calendar.MINUTE, 0); calendar.set(Calendar.HOUR_OF_DAY, 0); //当前时间日期 + 1,即下一天 calendar.add(Calendar.DATE, 1); SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd", Locale.US); currentDateString = df.format(new Date()); // if there is an existing default log file, use the date last modified instead of the current date if (Files.exists(defaultLogFile)) { try { currentDateString = df.format(new Date(Files.getLastModifiedTime(defaultLogFile).toMillis())); } catch(IOException e){ // ignore. use the current date if exception happens. } } //rotate 时机是下一天的 0 点 changeOverPoint = calendar.getTimeInMillis(); } ``` # accesslog 占位符 其实 Undertow 中的 accesslog 占位符,就是之前我们提到的 Undertow Listener 解析请求后抽象的 HTTP server exchange 的属性。 [官网文档](https://undertow.io/undertow-docs/undertow-docs-2.1.0/#exchange-attributes)的表格并不是最全的,并且注意点并没有说明,例如某些占位符必须打开某些 Undertow 特性才能使用等等。这里我们列出下。 首先先提出一个注意点,参数占位符,例如 `%{i,你要看的header值}` 查看 header 的某个 key 的值。**逗号后面注意不要有空格,因为这个空格会算入 key 里面导致拿不到你想要的 key**。 ## 请求相关属性 | 描述 | 缩写占位符 | 全名占位符 | 参数占位符 | 源码 | |------|-------|------|------|------| | 请求传输协议,等价于**请求协议** | 无 | `%{TRANSPORT_PROTOCOL}` | 无 | [`TransportProtocolAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/TransportProtocolAttribute.java) | | 请求模式,例如 http、https 等 | | `%{SCHEME}` | 无 | [`RequestSchemeAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/RequestSchemeAttribute.java) | | 请求协议,例如 `HTTP/1.1` 等 | `%H` | `%{PROTOCOL}` | 无 | [`RequestProtocolAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/RequestProtocolAttribute.java) | | 请求方法,例如 GET、POST 等 | `%m` | `%{METHOD}` | 无 | [`RequestMethodAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/RequestMethodAttribute.java) | | 请求 Header 的某一个值 | 无 | 无 | `%{i,你要看的header值}` | [`RequestHeaderAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/RequestHeaderAttribute.java)| | Cookie 的某一个值| 无 | 无 | `%{c,你要看的cookie值}` 或者 `%{req-cookie,你要看的cookie值}` | 分别对应 [`CookieAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/CookieAttribute.java) 和 [`RequestCookieAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/RequestCookieAttribute.java) | | 路径参数 PathVariable 由于并没有被 Undertow 的 Listener 或者 Handler 解析处理,所以拦截不到,无法确认是否是一个 PathVariable 还是就是 url 路径。所以,**PathVariable 的占位符是不会起作用的**。 | 无 | 无 | `%{p, 你想查看的路径参数 key }` | [`PathParameterAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/PathParameterAttribute.java) | | 请求参数,即 url 的 ? 之后键值对,这里可以选择查看某个 key 的值。| 无 | 无 | `%{q, 你想查看的请求参数 key}` | [`QueryParameterAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/QueryParameterAttribute.java) | | 请求参数字符串,即 url 的 ? 之后的所有字符} |`%q`(不包含 ?)|`%{QUERY_STRING}`(不包含 ?);`%{BARE_QUERY_STRING}`(包含 ?)| 无 | [`QueryStringAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/QueryStringAttribute.java) | |请求相对路径(在 Spring Boot 环境下,大多数情况 RequestPath 和 RelativePath 还有 ResolvedPath 是等价的),即除去 host,port,请求参数字符串的路径 | `%R` | `%{RELATIVE_PATH}` 或者 `%{REQUEST_PATH}` 或者 `%{RESOLVED_PATH}` | 无 | 分别对应 [`RelativePathAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/RelativePathAttribute.java) 和 [`RequestPathAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/RequestPathAttribute.java) 和 [`ResolvedPathAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/ResolvedPathAttribute.java)| | 请求整体字符串,包括请求方法,请求相对路径,请求参数字符串,请求协议,例如 `Get /test?a=b HTTP/1.1` | `%r` | `%{REQUEST_LINE}` | 无 | [`RequestLineAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/RequestLineAttribute.java) | | 请求 URI,包括请求相对路径,请求参数字符串 | `%U` | `%{REQUEST_URL}` | 无 | [`RequestURLAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/RequestURLAttribute.java) | | 处理请求的线程 | `%I` | `%{THREAD_NAME}` | 无 | [`ThreadNameAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/ThreadNameAttribute.java) | 注意: 1. 路径参数 PathVariable 由于并没有被 Undertow 的 Listener 或者 Handler 解析处理,所以拦截不到,无法确认是否是一个 PathVariable 还是就是 url 路径。所以,**PathVariable 的占位符是不会起作用的**。 ## 请求地址相关 | 描述 | 缩写占位符 | 全名占位符 | 参数占位符 | 源码 | |------|-------|------|------|------| |host 和 port,一般就是 HTTP 请求 Header 中的 Host 值,如果 Host 为空则获取本地地址和端口,如果没获取到端口则根据协议用默认端口(http:80,,https:443)| 无 | `%{HOST_AND_PORT}` | 无 | [`HostAndPortAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/HostAndPortAttribute.java) | |请求本地地址 IP| `%A` | `%{LOCAL_IP}` | 无 | [`LocalIPAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/LocalIPAttribute.java)| |请求本地端口 Port| `%p` | `%{LOCAL_PORT}` | 无 | [`LocalPortAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/LocalPortAttribute.java)| |请求本地主机名,一般就是 HTTP 请求 Header 中的 Host 值,如果 Host 为空则获取本地地址 | `%v` | `%{LOCAL_SERVER_NAME}` | 无 | [`LocalServerNameAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/LocalServerNameAttribute.java)| |请求远程主机名,通过连接获取远端的主机地址 | `%h` | `%{REMOTE_HOST}` | 无 | [`RemoteHostAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/RemoteHostAttribute.java)| |请求远程 IP,通过连接获取远端的 IP | `%a` | `%{REMOTE_IP}` | 无 | [`RemoteIPAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/RemoteIPAttribute.java)| 注意: 1. 请求的远程地址我们一般不从请求连接获取,而是通过 Http Header 里面的 `X-forwarded-for` 或者 `X-real-ip` 等获取,因为现在请求都是通过各种 ***,负载均衡器发上来的。 ## 响应相关属性 | 描述 | 缩写占位符 | 全名占位符 | 参数占位符 | 源码 | |------|-------|------|------|------| | 发送的字节数大小,除了 Http Header 以外 | `%b` (如果为空就是 -) 或者 `%B` (如果为空就是 0) | `%{BYTES_SENT}` (如果为空就是 0) | 无 | [`BytesSentAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/BytesSentAttribute.java)| | accesslog 时间,这个不是收到请求的时间,而是响应的时间 | `%t` | `%{DATE_TIME}` | `%{time, 你自定义的 java 中 SimpleDateFormat 的格式}` | [`DateTimeAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/DateTimeAttribute.java)| | HTTP 响应状态码 | `%s` | `%{RESPONSE_CODE}` | 无 | [`ResponseCodeAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/ResponseCodeAttribute.java)| | HTTP 响应原因 | 无 | `%{RESPONSE_REASON_PHRASE}` | 无 | [`ResponseReasonPhraseAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/ResponseReasonPhraseAttribute.java)| | 响应 Header 的某一个值 | 无 | 无 | `%{o,你要看的header值}` | [`ResponseHeaderAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/ResponseHeaderAttribute.java)| | 响应 Cookie 的某一个值 | 无 | 无 | `%{resp-cookie,你要看的cookie值}` | [`ResponseCookieAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/ResponseCookieAttribute.java)| | 响应时间,**默认 undertow 没有开启请求时间内统计,需要打开才能统计响应时间** | `%D`(毫秒,例如 56 代表 56ms) `%T`(秒,例如 5.067 代表 5.067 秒) | `%{RESPONSE_TIME}`(等价于 `%D`) `%{RESPONSE_TIME_MICROS}` (微秒) `%{RESPONSE_TIME_NANOS}`(纳秒) | 无 | [`ResponseTimeAttribute`](https://github.com/undertow-io/undertow/blob/2.2.7.Final/core/src/main/java/io/undertow/attribute/ResponseTimeAttribute.java)| 注意:**默认 undertow 没有开启请求时间内统计,需要打开才能统计响应时间**,如何开启呢?通过注册一个 `WebServerFactoryCustomizer` 到 Spring ApplicationContext 中即可。请看下面的代码(项目地址:[https://github.com/HashZhang/spring-cloud-scaffold/blob/master/spring-cloud-iiford/](https://github.com/HashZhang/spring-cloud-scaffold/blob/master/spring-cloud-iiford/)): [`spring.factories`(省略无关代码)](https://github.com/HashZhang/spring-cloud-scaffold/blob/master/spring-cloud-iiford/spring-cloud-iiford-service-common/src/main/resources/META-INF/spring.factories) ``` # AutoConfiguration org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.github.hashjang.spring.cloud.iiford.service.common.auto.UndertowAutoConfiguration ``` [`UndertowAutoConfiguration`](https://github.com/HashZhang/spring-cloud-scaffold/blob/master/spring-cloud-iiford/spring-cloud-iiford-service-common/src/main/java/com/github/hashjang/spring/cloud/iiford/service/common/auto/UndertowAutoConfiguration.java) ``` //设置proxyBeanMethods=false,因为没有 @Bean 的方法互相调用需要每次返回同一个 Bean,没必要代理,关闭增加启动速度 @Configuration(proxyBeanMethods = false) @Import(WebServerConfiguration.class) public class UndertowAutoConfiguration { } ``` [`WebServerConfiguration`](https://github.com/HashZhang/spring-cloud-scaffold/blob/master/spring-cloud-iiford/spring-cloud-iiford-service-common/src/main/java/com/github/hashjang/spring/cloud/iiford/service/common/undertow/WebServerConfiguration.java) ``` //设置proxyBeanMethods=false,因为没有 @Bean 的方法互相调用需要每次返回同一个 Bean,没必要代理,关闭增加启动速度 @Configuration(proxyBeanMethods = false) public class WebServerConfiguration { @Bean public WebServerFactoryCustomizer<ConfigurableUndertowWebServerFactory> undertowWebServerAccessLogTimingEnabler(ServerProperties serverProperties) { return new DefaultWebServerFactoryCustomizer(serverProperties); } } ``` [`DefaultWebServerFactoryCustomizer`](https://github.com/HashZhang/spring-cloud-scaffold/blob/master/spring-cloud-iiford/spring-cloud-iiford-service-common/src/main/java/com/github/hashjang/spring/cloud/iiford/service/common/undertow/DefaultWebServerFactoryCustomizer.java) ``` public class DefaultWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableUndertowWebServerFactory> { private final ServerProperties serverProperties; public DefaultWebServerFactoryCustomizer(ServerProperties serverProperties) { this.serverProperties = serverProperties; } @Override public void customize(ConfigurableUndertowWebServerFactory factory) { String pattern = serverProperties.getUndertow().getAccesslog().getPattern(); // 如果 accesslog 配置中打印了响应时间,则打开记录请求开始时间配置 if (logRequestProcessingTiming(pattern)) { factory.addBuilderCustomizers(builder -> builder.setServerOption(UndertowOptions.RECORD_REQUEST_START_TIME, true)); } } private boolean logRequestProcessingTiming(String pattern) { if (StringUtils.isBlank(pattern)) { return false; } //判断 accesslog 是否配置了查看响应时间 return pattern.contains(ResponseTimeAttribute.RESPONSE_TIME_MICROS) || pattern.contains(ResponseTimeAttribute.RESPONSE_TIME_MILLIS) || pattern.contains(ResponseTimeAttribute.RESPONSE_TIME_NANOS) || pattern.contains(ResponseTimeAttribute.RESPONSE_TIME_MILLIS_SHORT) || pattern.contains(ResponseTimeAttribute.RESPONSE_TIME_SECONDS_SHORT); } } ``` ## 其他 还有安全相关的属性(SSL 相关,登录认证 Authentication 相关),微服务内部调用一般用不到,我们这里就不赘述了。 其它内置的属性,在 Spring Boot 环境下一般用不到,我们这里就不讨论了。 ## 举例 我们最开始配置的 accesslog 的例子请求返回如下( JSON 格式化之后的结果): ``` { "transportProtocol": "http/1.1", "scheme": "http", "protocol": "HTTP/1.1", "method": "GET", "reqHeaderUserAgent": "PostmanRuntime/7.26.10", "cookieUserId": "testRequestCookieUserId", "queryTest": "1", "queryString": "?test=1&query=2", "relativePath": "/test, /test, -", "requestLine": "GET /test?test=1&query=2 HTTP/1.1", "uri": "/test", "thread": "XNIO-2 task-1", "hostPort": "127.0.0.1:8102", "localIp": "127.0.0.1", "localPort": "8102", "localServerName": "127.0.0.1", "remoteIp": "127.0.0.1", "remoteHost": "127.0.0.1", "bytesSent": "26", "time": "2021-04-08 00:07:50.410", "status": "200", "reason": "OK", "respHeaderUserSession": "testResponseHeaderUserSession", "respCookieUserId": "testResponseCookieUserId", "timeUsed": "3683ms, 3.683s, 3683ms, 3683149 us, 3683149200 ns", } ```

优秀的个人博客,低调大师

Go 语言社区系统 bbs-go 3.2.3 发布,UI 全新升级

更新内容 全新的话题UI交互 全新动态功能 优化话题详情页UI 修复评论数量显示错误的问题 优化后台话题管理功能 前台界面提供话题的推荐功能 优化头像显示组件 其他多处细节调整和BUG修复 发布地址 github:https://github.com/mlogclub/bbs-go/releases/tag/v3.2.3 gitee:https://gitee.com/mlogclub/bbs-go/releases/v3.2.3 功能简介 功能预览 相关链接 github:https://github.com/mlogclub/bbs-go gitee:https://gitee.com/mlogclub/bbs-go 演示站:https://mlog.club 帮助文档:http://docs.bbs-go.com/ 问题反馈:https://mlog.club/topic/create 功能建议收集:https://mlog.club/topic/609

优秀的个人博客,低调大师

Go 语言社区系统 bbs-go3.2.2 发布,UI 全新升级

更新内容 Feature:帖子、动态功能合并,动态功能更加完善 Feature:全新的帖子信息流展示样式 Feature:记录帖子最后回复人、最后回复时间 Feature:新增签到排行榜 Feature:帖子详情页支持推荐功能 Enhancement:优化头像的存储和加载方式 More:多处细节和功能优化 发布地址 github:https://github.com/mlogclub/bbs-go/releases/tag/v3.2.2 gitee:https://gitee.com/mlogclub/bbs-go/releases/v3.2.2 功能简介 功能预览 相关链接 github:https://github.com/mlogclub/bbs-go gitee:https://gitee.com/mlogclub/bbs-go 演示站:https://mlog.club 帮助文档:http://docs.bbs-go.com/ 问题反馈:https://mlog.club/topic/create 功能建议收集:https://mlog.club/topic/609

资源下载

更多资源
Spring

Spring

Spring框架(Spring Framework)是由Rod Johnson于2002年提出的开源Java企业级应用框架,旨在通过使用JavaBean替代传统EJB实现方式降低企业级编程开发的复杂性。该框架基于简单性、可测试性和松耦合性设计理念,提供核心容器、应用上下文、数据访问集成等模块,支持整合Hibernate、Struts等第三方框架,其适用范围不仅限于服务器端开发,绝大多数Java应用均可从中受益。

Rocky Linux

Rocky Linux

Rocky Linux(中文名:洛基)是由Gregory Kurtzer于2020年12月发起的企业级Linux发行版,作为CentOS稳定版停止维护后与RHEL(Red Hat Enterprise Linux)完全兼容的开源替代方案,由社区拥有并管理,支持x86_64、aarch64等架构。其通过重新编译RHEL源代码提供长期稳定性,采用模块化包装和SELinux安全架构,默认包含GNOME桌面环境及XFS文件系统,支持十年生命周期更新。

Sublime Text

Sublime Text

Sublime Text具有漂亮的用户界面和强大的功能,例如代码缩略图,Python的插件,代码段等。还可自定义键绑定,菜单和工具栏。Sublime Text 的主要功能包括:拼写检查,书签,完整的 Python API , Goto 功能,即时项目切换,多选择,多窗口等等。Sublime Text 是一个跨平台的编辑器,同时支持Windows、Linux、Mac OS X等操作系统。

WebStorm

WebStorm

WebStorm 是jetbrains公司旗下一款JavaScript 开发工具。目前已经被广大中国JS开发者誉为“Web前端开发神器”、“最强大的HTML5编辑器”、“最智能的JavaScript IDE”等。与IntelliJ IDEA同源,继承了IntelliJ IDEA强大的JS部分的功能。

用户登录
用户注册