首先是全局监控,我们需要掌握系统的上下游:应用拓扑、RPC提供者/消费者、MQ生产者/消费者、数据集群(mysql/redis/es等)。除了整体的监控,还需要看到接口调用量、成功率、失败数、错误日志数等指标。
再说机器监控,整个服务在运行期间,会部署多台机器提供支持。其中不可避免就会出现某些机器故障,这里我们就需要机器维度的监控,如TCP连接数、网络连通性、出入流量、磁盘指标、CPU指标、内存指标等。一旦监控到某些指标不合理,就需要及时的告警通知,进行流量摘除,并及时置换机器。
一个系统能运行,底层必然离不开诸多的数据支持。比如mysql、redis、es、rpc、mq等。我们要监控各个数据源的集群健康度,如mysql磁盘使用率、主从延迟、慢sql等,redis的大key、热key、同步延迟、内存使用率,es磁盘使用率、CPU使用率等,rpc调用时延,mq生产/消费详情,是否存在消费积压等。
有了机器和数据的稳定支持,接下来就需要关注服务细节,我们对外提供服务,要梳理调用链路,并将其执行情况生成对应的监控看板。在流量高峰期或者日常,我们可以通过链路看板观察服务链路中各个节点的运行情况。
如上图的调用链路,我们只要在核心节点处进行监控埋点,最终就可依据节点监控生成调用链路看板。
调用链路的监控形成需要依赖于各个节点的埋点,即接口监控。在编码期间我们可以通过抽象层、过滤器、拦截器、AOP等统一埋点,另外对于一些细节的逻辑,需要手动埋点。这里可以监控到接口的的tp、调用量、可用率、机房流量、机器流量等。
有了接口维度的监控,但在一些核心数据处理或业务逻辑中还存在一些需要业务监控的场景,如业务异常监控、分支执行情况监控等。
上文说明了服务调用链路、接口、业务监控都是服务内部的监控,偏向于业务逻辑。作为一个java服务集群,我们还需要关注服务自身的虚拟机情况。这里就需要监控jvm的指标,如内存使用率(堆内存/堆外内存)、CPU使用率、GC情况(老年代、新生代GC次数和耗时)、线程数等。
在mysql、redis、mq、es、配置中心等数据源要适当的打印出入参日志。为什么是适当呢,有的节点请求响应数据很大,针对这种情况可以只输出其中的一个元素,或者集合大小即可,防止频繁打印大对象导致磁盘使用率过高。
在链路中的关键节点要打印出入参,一方面在测试环境验证逻辑是否合理(如果有单元测试那就另说了),一方面线上出问题之后排查起来更容易。主要是核心业务流程的入口与出口、复杂算法的执行点、数据转换的关键步骤等。
一次用户请求往往需要经过多个系统、多个服务节点的协同处理才能完成。一次请求在各个系统要有唯一的链路id,对于排查自身系统来说可以串联日志,对于上下游来说也更精准的定位问题。
日志打印中要体现线程id,不然有些异步逻辑无法串联支路。在多线程并发场景下,线程信息也有助于排查资源竞争、死锁等问题。
打印日志时使用占位符可以走预检查,延迟计算,避免日志级别调整后出现无效的计算(如JSON.toJsonString(obj))。
集群部署可以解决机器故障的问题,跨机房可以解决机房故障的问题。那么是否需要某些策略来保障这个集群能发挥出最大的性能呢?这个就需要提到单元化,即北京周围用户访问北京的数据中心,上海周围用户访问上海的数据中心,其中底层数据共享通过数据同步机制进行复制。
是一款高性能的 HTTP 和反向代理服务器,同时也具备强大的负载均衡能力。它支持多种负载均衡算法,如轮询、加权轮询、IP 哈希等,并且能够根据服务器的响应时间动态调整请求分配。
- 优点:轻量级,资源占用少,性能出色,能够处理大量的并发请求;配置简单灵活,易于上手;支持多种协议,如 HTTP、HTTPS、WebSocket 等。
- 缺点:主要适用于 HTTP 协议的负载均衡,对于其他协议的支持相对有限;在处理大规模流量时,可能需要进行复杂的优化配置。
是一款支持多种协议的负载均衡器,可用于 TCP、HTTP、SSL 等协议的流量分发。它提供了丰富的负载均衡算法和健康检查机制,能够实时监测服务器的状态,确保请求被分配到健康的服务器上。
- 优点:功能强大,支持多种协议和负载均衡算法,适用于多种应用场景;具备良好的性能和稳定性,能够处理高并发请求;提供详细的统计信息和监控功能,便于系统管理和优化。
- 缺点:配置相对复杂,对于初学者来说可能需要一定的学习成本;在处理某些复杂的业务逻辑时,可能需要进行额外的开发和定制。
是基于 Linux 内核的负载均衡解决方案,工作在网络层,可对 TCP、UDP 等协议的流量进行分发。它提供了多种负载均衡模式,如 NAT 模式、DR 模式、TUN 模式等,能够满足不同的网络环境和业务需求。
- 优点:性能极高,能够处理大规模的网络流量,适用于高并发的应用场景;基于内核实现,稳定性好,可靠性高;支持多种负载均衡模式,可根据实际需求进行灵活选择。
- 缺点:配置相对复杂,需要对 Linux 内核和网络原理有深入的了解;主要适用于 TCP、UDP 协议的负载均衡,对于应用层协议的支持相对较少。
灰度发布本质上是一种平滑过渡的发布策略,它像给新功能戴上“观察滤镜”,在确保核心用户群稳定体验的同时,小范围验证新功能的可靠性与兼容性。除了按人群、区域、IP划分外,灰度还常基于用户画像标签(如消费等级、使用频次)、设备类型(iOS/Android)等条件精准筛选目标群体。例如某电商APP上线“AR虚拟试衣”功能时,先对年轻女性用户、高配机型用户开放,优先获取更适配的反馈。
在具体实施流程中,灰度发布通常分为多个阶段:首先是“冒烟测试”阶段,面向内部员工或测试用户群,在真实环境中快速验证功能基础逻辑;接着进入“小流量灰度”,将新功能暴露给1% - 5%的真实用户,重点监测性能指标(如响应时间、接口成功率)与用户行为数据(页面停留时长、功能使用率);最后通过逐步放量,每次递增5% - 10%用户,直至全量发布。
灰度发布的核心价值不仅在于故障止损,还体现在业务决策层面。通过灰度期收集的用户反馈,产品团队能及时调整功能设计,例如某社交APP在灰度测试“语音聊天匹配”功能时,根据用户投诉调整了匹配算法;同时,灰度也是A/B测试的天然载体,可并行验证多个方案,如不同界面布局、推荐策略的转化效果。此外,灰度还能缓解技术团队的发布压力,分阶段监控日志与监控指标,定位问题的时间成本大幅降低。当故障发生时,完善的灰度架构支持“一键回滚”,通过流量调度策略快速将用户切回稳定版本,将故障影响时长控制在分钟级。
为了保证在大流量场景下服务不崩,限流是必须坚守的底线。要评估服务集群中的压力点,如数据库、缓存等、以及单机的压力点,如CPU、内存占用等。
令牌桶算法是一种常用的限流算法,它以固定的速率向桶中添加令牌,每个请求需要从桶中获取一个或多个令牌才能被处理。如果桶中没有足够的令牌,请求将被拒绝。
import redis.clients.jedis.Jedis;
public class TokenBucketRateLimiter {
private static final String REDIS_KEY = "token_bucket";
private static final int CAPACITY = 100; // 令牌桶容量
private static final int RATE = 10; // 令牌生成速率(每秒)
private static final long INTERVAL = 1000; // 时间间隔(毫秒)
private Jedis jedis;
public TokenBucketRateLimiter() {
this.jedis = new Jedis("localhost", 6379);
// 初始化令牌桶
jedis.set(REDIS_KEY, String.valueOf(CAPACITY));
}
public boolean tryAcquire() {
long currentTime = System.currentTimeMillis();
// 计算当前应该拥有的令牌数
long tokens = Long.parseLong(jedis.get(REDIS_KEY));
long newTokens = Math.min(CAPACITY, tokens + (currentTime - lastUpdateTime) * RATE / INTERVAL);
if (newTokens > 0) {
// 消耗一个令牌
jedis.set(REDIS_KEY, String.valueOf(newTokens - 1));
lastUpdateTime = currentTime;
return true;
}
return false;
}
private long lastUpdateTime = System.currentTimeMillis();
}
在 Redis 中,可以使用有序集合(zset)来实现毫秒级的限流。其核心思路是将每个请求的时间戳作为分数存储在有序集合里,通过移除过期的时间戳并统计当前集合内的元素数量,以此判断是否超出限流阈值。
public class ZSetRateLimiter {
private static final String REDIS_HOST = "localhost";
private static final int REDIS_PORT = 6379;
private static final int LIMIT = 10; // 限流阈值
private static final long WINDOW = 1000; // 时间窗口,单位毫秒
private Jedis jedis;
public ZSetRateLimiter() {
this.jedis = new Jedis(REDIS_HOST, REDIS_PORT);
}
public boolean tryAcquire(String key) {
long currentTime = System.currentTimeMillis();
// 移除时间窗口外的元素
jedis.zremrangeByScore(key, 0, currentTime - WINDOW);
// 添加当前请求的时间戳
jedis.zadd(key, currentTime, UUID.randomUUID().toString());
// 统计当前时间窗口内的请求数量
Long count = jedis.zcard(key);
return count <= LIMIT;
}
}
突发型 RateLimiter 基于经典的令牌桶算法实现,令牌以固定的速率添加到令牌桶中。当有请求到来时,会尝试从令牌桶中获取所需数量的令牌。如果桶中有足够的令牌,请求会立即被处理;如果没有足够的令牌,请求会被阻塞,直到桶中生成足够的令牌。
适用场景
import com.google.common.util.concurrent.RateLimiter;
public class BurstyRateLimiterExample {
public static void main(String[] args) {
// 创建一个每秒允许 5 个请求的突发型 RateLimiter
RateLimiter rateLimiter = RateLimiter.create(5.0);
for (int i = 0; i < 10; i++) {
// 获取一个令牌
double waitTime = rateLimiter.acquire();
System.out.println("第 " + i + " 次请求,等待时间:" + waitTime + " 秒");
}
}
}
预热型 RateLimiter 在令牌桶算法的基础上增加了预热期的概念。在系统启动初期,令牌的生成速率较低,随着时间的推移,令牌的生成速率逐渐增加,直到达到预设的稳定速率。这样可以让系统在启动阶段有一个逐渐适应流量的过程,避免系统在启动初期因突然到来的大量请求而崩溃。
适用场景
import com.google.common.util.concurrent.RateLimiter;
import java.util.concurrent.TimeUnit;
public class WarmingUpRateLimiterExample {
public static void main(String[] args) {
// 创建一个每秒允许 5 个请求,预热时间为 5 秒的预热型 RateLimiter
RateLimiter rateLimiter = RateLimiter.create(5.0, 5, TimeUnit.SECONDS);
for (int i = 0; i < 10; i++) {
// 获取一个令牌
double waitTime = rateLimiter.acquire();
System.out.println("第 " + i + " 次请求,等待时间:" + waitTime + " 秒");
}
}
}
隔离
复制
降级
超时
异步
并发
同构
异构
灾备
故障转移
分库分表