共计 13403 个字符,预计需要花费 34 分钟才能阅读完成。
在互联网系统中,服务提供方(upstream)因访问压力过大而响应变慢或失败,服务发起方(downstream)为了保护系统整体的可用性,可以临时暂停对服务提供方的调用,这种牺牲局部,保全整体的措施就叫做熔断。
限流可以认为服务降级的一种,限流就是限制系统的输入和输出流量已达到保护系统的目的。一般来说系统的吞吐量是可以被测算的,为了保证系统的稳定运行,一旦达到的需要限制的阈值,就需要限制流量并采取一些措施以完成限制流量的目的。比如:延迟处理,拒绝处理,或者部分拒绝处理等等。一般也会算在熔断中。
可靠性是微服务架构的关键,熔断(Circuit breakers)是减少服务异常和降低服务延迟的一种设计模式,如果在一定时间内服务累计发生错误的次数超过了预先定义的阈值,就会将该错误的服务从负载均衡池中移除(如下图),当超过一段时间后,又会将服务再移回到服务负载均衡池。
熔断主要是无感的处理服务异常并保证不会发生级联甚至雪崩的服务异常。在微服务方面体现是对异常的服务情况进行快速失败,它对已经调用失败的服务不再会继续调用,如果仍需要调用此异常服务,它将立刻返回失败。与此同时,它一直监控服务的健康状况,一旦服务恢复正常,则立刻恢复对此服务的正常访问。这样的快速失败策略可以降低服务负载压力,很好地保护服务免受高负载的影响。
熔断器对比 Hystrix vs Istio
Hystrix vs Istio 两大熔断器对比:
Hystrix 是 Netflix OSS 的一部分,是微服务领域的领先的熔断工具。Hystrix 可以被视为白盒监控工具,而 Istio 可以被视为黑盒监控工具,主要是因为 Istio 从外部监控系统并且不知道系统内部如何工作。另一方面,每个服务中有 Hystrix 来获取所需的数据。
Istio 是无缝衔接服务,istio 可以在不更改应用程序代码的情况下配置和使用。Hystrix 的使用需要更改每个服务来引入 Hystrix libraries。
Istio 提高了网格中服务的可靠性和可用性。但是,应用程序需要处理错误并有一定的 fall back 行为。例如当负载平衡池中的所有服务实例都出现异常时,Envoy 将返回 HTTP 503。当上游服务返回 HTTP 503 错误,则应用程序需要采取回退逻辑。与此同时,Hystrix 也提供了可靠的 fall back 实现。它允许拥有所有不同类型的 fall backs:单一的默认值、缓存或者去调用其他服务。
envoy 对应用程序来说几乎完全无感和透明。Hystrix 则必须在每个服务调用中嵌入 Hystrix 库。
Istio 的熔断应用几乎无语言限制,但 Hystrix 主要针对的是 Java 应用程序。
Istio 是使用了黑盒方式来作为一种代理管理工具。它实现起来很简单,不依赖于底层技术栈,而且可以在部署后也可以进行配置和修改。Hystrix 需要在代码级别处理断路器,可以设置级联和一些附加功能,它需要在开发阶段时做出降级决策,后期优化配置成本高。
Istio 限流
Istio 无需对代码进行任何更改就可以为应用增加熔断和限流功能。Istio 中熔断和限流在 DestinationRule 的 CRD 资源的 TrafficPolicy 中设置,一般设置连接池(ConnectionPool)限流方式和异常检测(outlierDetection)熔断方式。两者 ConnectionPool 和 outlierDetection 各自配置部分参数,其中参数有可能存在对方的功能,并没有很严格的区分出来,如主要进行限流设置的 ConnectionPool 中的 maxPendingRequests 参数,最大等待请求数,如果超过则也会暂时的熔断。
连接池(ConnectionPool)设置
ConnectionPool 可以对上游服务的并发连接数和请求数进行限制,适用于 TCP 和 HTTP。ConnectionPool 又称之是限流。
官方定义的属性
设置在 DestinationRule 中的配置如下图:
连接池相关参数解析
TCP 设置
Tcp 连接池设置 http 和 tcp 上游连接的设置。相关参数设置如下:
maxConnections:到目标主机的 HTTP1/TCP 最大连接数量,只作用于 http1.1,不作用于 http2,因为后者只建立一次连接。
connectTimeout:tcp 连接超时时间,默认单位秒。也可以写其他单位,如 ms。
tcpKeepalive:如果在套接字上设置 SO_KEEPALIVE 可以确保 TCP 存活
TCP 的 TcpKeepalive 设置:
Probes:在确定连接已死之前,在没有响应的情况下发送的 keepalive 探测的最大数量。默认值是使用系统级别的配置(除非写词参数覆盖,Linux 默认值为 9)。
Time:发送 keep-alive 探测前连接存在的空闲时间。默认值是使用系统的配置(除非写此参数,Linux 默认值为 7200s(即 2 小时)。
interval:探测活动之间的时间间隔。默认值是使用系统的配置(除非被覆盖,Linux 默认值为 75 秒)。
HTTP 设置
http 连接池设置用于 http1.1/HTTP2/GRPC 连接。
http1MaxPendingRequests:http 请求 pending 状态的最大请求数,从应用容器发来的 HTTP 请求的最大等待转发数,默认是 1024。
http2MaxRequests:后端请求的最大数量,默认是 1024。
maxRequestsPerConnection:在一定时间内限制对后端服务发起的最大请求数,如果超过了这个限制,就会开启限流。如果将这一参数设置为 1 则会禁止 keepalive 特性;
idleTimeout:上游连接池连接的空闲超时。空闲超时被定义为没有活动请求的时间段。如果未设置,则没有空闲超时。当达到空闲超时时,连接将被关闭。注意,基于请求的超时意味着 HTTP/2ping 将无法保持有效连接。适用于 HTTP1.1 和 HTTP2 连接;
maxRetries:在给定时间内,集群中所有主机都可以执行的最大重试次数。默认为 3。
与 Envoy 参数对比
熔断和限流是分布式系统的重要组成部分,优点是快速失败并迅速向下游反馈。Istio 是通过 Envoy Proxy 来实现熔断和限流机制的,Envoy 强制在网络层面配置熔断和限流策略,这样就不必为每个应用程序单独配置或重新编程。Envoy 支持各种类型的全分布式 (不协调) 限流。
IstioConnectionPool 与 Envoy 的限流参数对照表:
Envoy paramether
Envoy upon object
Istio parameter
Istio upon ojbect
max_connections
cluster.circuit_breakers
maxConnections
TCPSettings
max_pending_requests
cluster.circuit_breakers
http1MaxPendingRequests
HTTPSettings
max_requests
cluster.circuit_breakers
http2MaxRequests
HTTPSettings
max_retries
cluster.circuit_breakers
maxRetries
HTTPSettings
connect_timeout_ms
cluster
connectTimeout
TCPSettings
max_requests_per_connection
cluster
maxRequestsPerConnection
HTTPSettings
maxConnections: 表示在任何给定时间内,Envoy 与上游集群建立的最大连接数,限制对后端服务发起的 HTTP/1.1 连接数。该配置仅适用于 HTTP/1.1 协议,因为 HTTP/2 协议可以在同一个 TCP 连接中发送多个请求,而 HTTP/1.1 协议在同一个连接中只能处理一个请求。如果超过了这个限制(即断路器溢出),集群的 upstream_cx_overflow 计数器就会增加。
maxPendingRequests: 表示待处理请求队列的长度,如果超过了这个限制,就会开启限流。因为 HTTP/2 是通过单个连接并发处理多个请求的,因此该策略仅在创建初始 HTTP/2 连接时有用,之后的请求将会在同一个 TCP 连接上多路复用。对于 HTTP/1.1 协议,只要没有足够的上游连接可用于立即分派请求,就会将请求添加到待处理请求队列中,因此该断路器将在该进程的生命周期内保持有效。如果该断路器溢出,集群的 upstream_rq_pending_overflow 计数器就会增加。
maxRequestsPerConnection: 表示在任何给定时间内,上游集群中主机可以处理的最大请求数,限制对后端服务发起的 HTTP/2 请求数。实际上,这适用于仅 HTTP/2 集群,因为 HTTP/1.1 集群由最大连接数断路器控制。如果该断路器溢出,集群的 upstream_rq_pending_overflow 计数器就会递增。
maxRetries: 在任何给定时间内,集群中所有主机都可以执行的最大重试次数。一般情况下,建议对偶尔的故障积极地进行断路重试,因为总体重试容量不会爆炸并导致大规模级联故障。如果这个断路器溢出,则集群的 upstream_rq_retry_overflow 计数器将增加。
envoy 新加参数(后期 istio 可能会增加)
maximumconcurrent connection pools:可以并发实例化的连接池的最大数量。一些特性,比如 Original SrcListener Filter,可以创建无限数量的连接池。当集群耗尽其并发连接池时将会回收空闲连接。如果不能回收,断路器就会溢出。这与连接池中的集群最大连接不同,连接池中的连接通常不会超时。Connections 自动清理; 连接池不需要。注意,为了使连接池发挥作用,它至少需要一个上游连接,因此这个值应该小于集群最大连接。
在上游集群和优先级上针对不同的组件,都可以分别进行单独的配置参数进行请求限制。通过统计可以观察到这些断路器的状态,包括断路器打开前剩余数量的断路器。注意,在 HTTP 请求下将会重新设置路由过滤器的 x -envoy-overloaded 报头。
举个例子
使用 istio 的 sample 中自带的 httpbin 案例分析。设置 maxConnections:1 以及 http1MaxPendingRequests: 1,表示如果超过了一个连接同时发起请求,Istio 就会限流,阻止后续的请求或连接。
先尝试通过单线程(NUM_THREADS=1)创建一个连接,并进行 5 次调用(默认值:NUM_CALLS_PER_CLIENT=5):
$ CLIENT_POD=$(kubectl get pod | grep httpbin-client | awk '{ print $1}')
$ kubectl exec -it $CLIENT_POD -c httpbin-client -- sh -c 'export URL_UNDER_TEST=http://httpbin:8000/get export NUM_THREADS=1 && java -jar http-client.jar'
using num threads: 1
Starting pool-1-thread-1 with numCalls=5 delayBetweenCalls=0 url=http://localhost:15001/get mixedRespTimes=false
pool-1-thread-1: successes=[5], failures=[0], duration=[545ms]
可以看到所有请求都通过了:successes=[5]
我们可以查询 istio-proxy 的状态,获取更多相关信息:
$ kubectl exec -it $CLIENT_POD -c istio-proxy -- sh -c 'curl localhost:15000/stats' | grep httpbin
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_cx_http1_total: 5
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_cx_overflow: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_total: 5
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_200: 5
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_2xx: 5
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_overflow: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_retry: 0
可以看出总共发送了 5 个 HTTP/1.1 连接,也就是 5 个请求,响应码均为 200。
下面尝试把线程数提高到 2 的结果如下:
kubectl exec -it $CLIENT_POD -c httpbin-client -- sh -c 'export URL_UNDER_TEST=http://httpbin:8000/get export NUM_THREADS=2 && java -jar http-client.jar'
using num threads: 2
Starting pool-1-thread-1 with numCalls=5 parallelSends=false delayBetweenCalls=0 url=http://httpbin:8000/get mixedRespTimes=false
Starting pool-1-thread-2 with numCalls=5 parallelSends=false delayBetweenCalls=0 url=http://httpbin:8000/get mixedRespTimes=false
pool-1-thread-1: successes=[3], failures=[2], duration=[96ms]
pool-1-thread-2: successes=[4], failures=[1], duration=[87ms]
查看 istio-proxy 的状态:
$ kubectl exec -it $CLIENT_POD -c istio-proxy -- sh -c 'curl localhost:15000/stats' | grep httpbin
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_cx_http1_total: 7
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_cx_overflow: 3
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_total: 10
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_200: 7
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_2xx: 7
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_503: 3
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_5xx: 3
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_overflow:
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_retry:0
...
总共发送了 10 个 HTTP/1 连接,只有 7 个被允许通过,剩余请求被断路器拦截了。其中 upstream_cx_overflow 的值为 3,表明 maxConnections 断路器起作用了。Istio-proxy 允许一定的冗余,你可以将线程数提高到 3,限流的效果会更明显。
再来测试一下 maxPendingRequests 断路器。前面已经将 maxPendingRequests 的值设置为 1,现在按照预想,我们只需要模拟在单个 HTTP/1.1 连接中同时发送多个请求,就可以触发该断路器开启限流。由于 HTTP/1.1 同一个连接只能处理一个请求,剩下的请求只能放到待处理请求队列中。通过限制待处理请求队列的长度,可以对恶意请求、DoS 和系统中的级联错误起到一定的缓解作用。
现在尝试通过单线程(NUM_THREADS=1)创建一个连接,并同时发送 20 个请求(PARALLEL_SENDS=true,NUM_CALLS_PER_CLIENT=20):
$ kubectl exec-it $CLIENT_POD -c httpbin-client -- sh -c 'export URL_UNDER_TEST=http://httpbin:8000/get export NUM_THREADS=1 export PARALLEL_SENDS=true export NUM_CALLS_PER_CLIENT=20 && java -jar http-client.jar'
using num threads:1
Starting pool-1-thread-1with numCalls=20 parallelSends=true delayBetweenCalls=0 url=http://httpbin:8000/get mixedRespTimes=false
finished batch 0
finished batch 5
finished batch 10
finished batch 15
pool-1-thread-1: successes=[16], failures=[4], duration=[116ms]
查询 istio-proxy 的状态:
$ kubectl exec-it $CLIENT_POD -c istio-proxy -- sh -c 'curl localhost:15000/stats'| grep httpbin | grep pending
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_active:0
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_failure_eject:0
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_overflow:4
cluster.outbound|8000||httpbin.default.svc.cluster.local.upstream_rq_pending_total:16
upstream_rq_pending_overflow 的值是 4,说明有 4 次调用触发了 maxPendingRequests
断路器的熔断策略,被标记为熔断。
Istio 熔断
熔断策略对集群中压力过大的上游服务起到一定的保护作用,有一种情况是集群中的某些节点完全崩溃,这种情况我们并不知晓。istio 引入了异常检测来完成熔断的功能,通过周期性的动态的异常检测来确定上游集群中的某些主机是否异常,如果发现异常,就将该主机从连接池中隔离出去,这就是异常值检测。也是一种熔断的实现,用于跟踪上游服务的状态。适用于 HTTP 和 TCP 服务。对于 HTTP 服务,API 调用连续返回 5xx 错误,则在一定时间内连接池拒绝此服务。对于 TCP 服务,一个主机连接超时次数或者连接失败次数达到一定次数时就认为是连接错误。
异常检测的原理
- 检测到了某个主机异常。
2. 如果到目前为止负载均衡池中还没有主机被隔离出去,将会立即隔离该异常主机;如果已经有主机被隔离出去,就会检查当前隔离的主机数是否低于设定的阈值(通过 envoy 中的 outlier_detection.max_ejection_percent 指定),如果当前被隔离的主机数量不超过该阈值,就将该主机隔离出去,否则不隔离。
3. 隔离不是永久的,会有一个时间限制。当主机被隔离后,该主机就会被标记为不健康,并且不会被加入到负载均衡池中,除非负载均衡处于恐慌模式。隔离时间等于 envoy 中的 outlier_detection.base_ejection_time_ms 的值乘以主机被隔离的次数。所以如果某个主机连续出现故障,会导致它被隔离的时间越来越长。
4. 经过了规定的隔离时间之后,被隔离的主机将会自动恢复过来,重新接受调用方的远程调用。通常异常检测会与主动健康检查一起用于全面的健康检查解决方案。
异常检测类型
连续 5xx 响应:如果上游主机连续返回一定数量的 5xx 响应,该主机就会被驱逐。注意,这里的 5xx 响应不仅包括返回的 5xx 状态码,也包括 HTTP 路由返回的一个事件(如连接超时和连接错误)。隔离主机所需的 5xx 响应数量由 consecutive_5xx 的值控制。
连续网关故障:如果上游主机连续返回一定数量的 “gatewayerrors”(502,503 或 504 状态码),该主机就会被驱逐。这里同样也包括 HTTP 路由返回的一个事件(如连接超时和连接错误)。隔离主机所需的连续网关故障数量由 consecutive_gateway_failure 的值控制。
调用成功率:基于调用成功率的异常检测类型会聚合集群中每个主机的调用成功率,然后根据统计的数据以给定的周期来隔离主机。如果该主机的请求数量小于 success_rate_request_volume 指定的值,则不会为该主机计算调用成功率,因此聚合的统计数据中不会包括该主机的调用成功率。如果在给定的周期内具有最小所需请求量的主机数小于 success_rate_minimum_hosts 指定的值,则不会对该集群执行调用成功率检测。
与 Envoy 的参数对比
Istio outlierDetection 与 Envoy 的异常检测参数对照表如下所示:
Envoy paramether
Envoy upon object
Istio parameter
Istio upon ojbect
consecutive_gateway_failure
cluster.outlier_detection
consecutiveErrors
outlierDetection
interval
cluster.outlier_detection
interval
outlierDetection
baseEjectionTime
cluster.outlier_detection
baseEjectionTime
outlierDetection
maxEjectionPercent
cluster.outlier_detection
maxEjectionPercent
outlierDetection
部分参数解析
常用的配置事例入下图:
consecutiveErrors:从连接池开始拒绝连接,已经连接失败的次数。当通过 HTTP 访问时,返回代码是 502、503 或 504 则视为错误。当访问不透明的 TCP 连接时,连接超时和连接错误 / 失败也会都视为错误。即将实例从负载均衡池中剔除,需要连续的错误(HTTP5XX 或者 TCP 断开 / 超时)次数。默认是 5。
Interval:拒绝访问扫描的时间间隔,即在 interval(1s)内连续发生 1 个 consecutiveErrors 错误,则触发服务熔断,格式是 1h/1m/1s/1ms,但必须大于等于 1ms。即分析是否需要剔除的频率,多久分析一次,默认 10 秒。
baseEjectionTime:最短拒绝访问时长。这个时间主机将保持拒绝访问,且如果决绝访问达到一定的次数。这允许自动增加不健康服务的拒绝访问时间,时间为 baseEjectionTime* 驱逐次数。格式:1h/1m/1s/1ms,但必须大于等于 1ms。实例被剔除后,至少多久不得返回负载均衡池,默认是 30 秒。
maxEjectionPercent:服务在负载均衡池中被拒绝访问(被移除)的最大百分比,负载均衡池中最多有多大比例被剔除,默认是 10%。
minHealthPercent:在健康模式下,外部检测可以和负载均衡池一样,有最小健康百分比的阈值。当健康主机百分比低于这个阈值,外部检测将禁用,同时 proxy 将会对所有主机进行负载均衡,包含健康和不健康的主机。通常 minHealthPercent+maxEjectionPercent<= 100%。默认值是 50%。
上面例子是设置 tcp 的连接池大小为 100 个连接,可以有 1000 个并发 HTTP2 请求。“reviews”服务的请求连接比不大于 10。此外,配置拒绝访问的时间间隔是 5 分钟,同时,任何连续 7 次返回 5XX 码的主机,将会拒绝访问 15 分钟。
举个例子
举例:设置的参数如下,该配置表示每秒钟扫描一次上游主机,连续失败 1 次返回 5xx 错误码的所有主机会被移出连接池 3 分钟。
我们通过调用一个 URL 来指定 httpbin 服务返回 502 状态码,以此来触发连续网关故障异常检测。总共发起 3 次调用,因为 outlierDetection 中的配置要求 Envoy 的异常检测机制必须检测到两个连续的网关故障才会将 httpbin 服务移除负载均衡池。
kubectl exec-it $CLIENT_POD -c httpbin-client -- sh -c 'export URL_UNDER_TEST=http://httpbin:8000/status/502 export NUM_CALLS_PER_CLIENT=3 && java -jar http-client.jar'
using num threads:1
Starting pool-1-thread-1with numCalls=3 parallelSends=false delayBetweenCalls=0 url=http://httpbin:8000/status/502 mixedRespTimes=false
pool-1-thread-1: successes=[0], failures=[3], duration=[99ms]
查看 istio-proxy 的状态:
kubectl exec -it $CLIENT_POD -c istio-proxy -- sh -c 'curl localhost:15000/stats' | grep httpbin | grep outlier
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_active: 1
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_consecutive_5xx: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_detected_consecutive_5xx: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_detected_consecutive_gateway_failure: 1
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_detected_success_rate: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_enforced_consecutive_5xx: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_enforced_consecutive_gateway_failure: 1
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_enforced_success_rate: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_enforced_total: 1
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_overflow: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_success_rate: 0
cluster.outbound|8000||httpbin.default.svc.cluster.local.outlier_detection.ejections_total: 0
确实检测到了连续网关故障,consecutive_gateway_failure 的值为 1。
例子 2:
kind: DestinationRule
apiVersion: networking.istio.io/v1alpha3
metadata:
name: testhttp
spec:
host: httpbin.org
trafficPolicy:
connectionPool:
http:
http1MaxPendingRequests: 1024
http2MaxRequests: 1024
outlierDetection:
baseEjectionTime: 10m
consecutiveErrors: 1
interval: 1s
maxEjectionPercent: 50
minHealthPercent: 50
subsets:
- labels:
version: v1
name: v1
- labels:
version: v2
name: v2
上述配置在实际测试中生效,subset 的 v1 中一个 pod 和 subset 的 v2 中一个 pod 返回 200(健康),subset 的 v1 中另一个 pod 返回 503(不健康),则 subset-v1 健康实例百分比是:
pod-v1-2/(pod-v1-1 + pod-v1-2) = 1/2 = 50%>= minHealthPercent=50%,
subset-v1 不健康实例百分比:
pod-v1-1/(pod-v1-1 + pod-v1-2) = 1/2 = 50% <=maxEjectionPercent=50%,则 subset 的 v1 中健康的实例百分比 50%>=50%(minHealthPercent)且不健康的实例百分比 50%<=50%(maxEjectionPercent),则服务熔断触发,异常检测生效,v1 的一个 pod 返回 503,服务实例被从服务负载均衡池中移除,实际观察到的现象就是 subset 的 v1 中另一个 pod 继续提供服务而 v1 的 pod 在接受 1 - 2 个请求之后便不再接收请求,而 subset 的 v2 中 pod 未受到影响,继续提供服务。
以上是个人对 Istio 中 Circuit Breaker 的简单学习,很乐意在云原生技术社区与各位技术同仁互相交流,共同提高!