1. 项目概述
StarRocks Agent是一个用于管理和监控StarRocks集群的关键组件。作为分布式数据库系统的重要组成部分,Agent负责节点状态采集、任务分发、配置管理等核心功能。在实际生产环境中,一个稳定可靠的Agent对于保障集群健康运行至关重要。
我曾在多个大规模生产集群中部署和维护过StarRocks Agent,深知其设计要点和常见问题。本文将分享从零开始构建一个功能完善的StarRocks Agent的完整过程,包括架构设计、核心功能实现、性能优化等关键环节。
2. 核心架构设计
2.1 整体架构解析
一个典型的StarRocks Agent采用分层架构设计,主要包含以下组件:
- 通信层:负责与FE(Frontend)节点和其他Agent节点的网络通信
- 任务调度层:处理来自FE的任务请求,管理本地任务队列
- 监控采集层:定期采集节点资源使用情况和数据库指标
- 配置管理层:管理节点配置,响应配置变更请求
- 日志处理层:收集、过滤和转发节点日志
这种分层设计使得各功能模块解耦,便于单独扩展和维护。在实际实现中,我们通常采用多线程模型,每个核心功能模块运行在独立的线程中,通过消息队列进行通信。
2.2 通信协议选择
Agent与FE之间的通信协议选择至关重要。基于性能和生产环境验证,我们推荐使用:
- Thrift RPC:作为StarRocks生态的标准通信协议,天然兼容现有系统
- HTTP REST:用于简单的状态检查和监控数据上报
- gRPC:适用于需要高性能双向通信的场景
在实现中,我们主要采用Thrift协议,因为它与StarRocks核心组件保持一致性,减少了协议转换的开销。以下是一个典型的Thrift接口定义示例:
thrift复制service AgentService {
// 心跳检测
HeartbeatResponse heartbeat(1:HeartbeatRequest request),
// 任务执行
TaskResponse executeTask(1:TaskRequest task),
// 配置更新
ConfigResponse updateConfig(1:ConfigRequest config),
// 指标上报
MetricResponse reportMetrics(1:MetricRequest metrics)
}
3. 核心功能实现
3.1 心跳机制实现
心跳是Agent最基础也是最重要的功能,它向FE证明自己的存活状态并交换基础信息。一个健壮的心跳机制应该包含:
- 定时触发:通常每3-5秒一次,可根据网络状况动态调整
- 超时重试:连续3次失败后应触发故障转移
- 增量上报:只上报变化的状态信息,减少网络开销
以下是Java实现的代码片段:
java复制public class HeartbeatThread extends Thread {
private static final int BASE_INTERVAL = 3000; // 3秒
private static final int MAX_RETRY = 3;
@Override
public void run() {
int retryCount = 0;
while (!isShutdown) {
try {
HeartbeatResponse response = sendHeartbeat();
retryCount = 0; // 重置重试计数
adjustInterval(response.getSuggestedInterval());
Thread.sleep(currentInterval);
} catch (Exception e) {
retryCount++;
if (retryCount >= MAX_RETRY) {
triggerFailover();
break;
}
Thread.sleep(currentInterval * 2); // 退避策略
}
}
}
private HeartbeatResponse sendHeartbeat() {
HeartbeatRequest request = new HeartbeatRequest(
nodeId, getLoadAverage(), getDiskUsage());
return feClient.heartbeat(request);
}
}
3.2 任务调度实现
Agent需要处理来自FE的各种任务,如:
- 数据加载(Bulk Load)
- 副本修复(Replica Repair)
- 快照制作(Snapshot)
- 数据均衡(Rebalance)
我们采用优先级队列+线程池的方式实现任务调度:
java复制public class TaskScheduler {
private PriorityBlockingQueue<Task> taskQueue =
new PriorityBlockingQueue<>(100, new TaskComparator());
private ThreadPoolExecutor executor;
public void init() {
executor = new ThreadPoolExecutor(
4, // 核心线程数
16, // 最大线程数
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new TaskThreadFactory()
);
new Thread(this::dispatchTask).start();
}
private void dispatchTask() {
while (!isShutdown) {
Task task = taskQueue.take();
executor.execute(() -> {
try {
TaskResult result = task.execute();
reportResult(task.getTaskId(), result);
} catch (Exception e) {
handleTaskFailure(task, e);
}
});
}
}
}
提示:任务线程池的配置需要根据节点硬件资源调整,通常建议:
- CPU密集型任务:线程数 ≈ CPU核心数
- IO密集型任务:线程数 ≈ CPU核心数 * 2
4. 监控采集与上报
4.1 系统指标采集
Agent需要定期采集以下系统指标:
- CPU使用率:用户态、系统态、空闲比例
- 内存使用:总量、已用、缓存、交换分区
- 磁盘IO:读写吞吐量、IOPS、使用率
- 网络:带宽使用、连接数、错误包数
在Linux系统下,我们可以通过/proc文件系统获取这些指标。以下是采集CPU使用率的示例:
java复制public class CpuStatsCollector {
public CpuStats collect() throws IOException {
String[] lines = FileUtils.readLines("/proc/stat");
String cpuLine = lines[0];
String[] parts = cpuLine.split("\\s+");
long user = Long.parseLong(parts[1]);
long nice = Long.parseLong(parts[2]);
long system = Long.parseLong(parts[3]);
long idle = Long.parseLong(parts[4]);
long iowait = Long.parseLong(parts[5]);
long total = user + nice + system + idle + iowait;
return new CpuStats(user, nice, system, idle, iowait, total);
}
}
4.2 数据库指标采集
除了系统指标,Agent还需要采集StarRocks特定的数据库指标:
- BE(Backend)状态:tablet数量、版本数、数据量
- 查询统计:QPS、延迟、错误率
- Compaction状态:积压任务数、进度
- 数据分布:各分区数据量、副本状态
这些指标通常通过StarRocks提供的HTTP接口获取:
java复制public class BeMetricsCollector {
private static final String BE_METRICS_URL = "http://localhost:8040/metrics";
public Map<String, Object> collect() {
String json = HttpUtils.get(BE_METRICS_URL);
return JsonUtils.parseMap(json);
}
}
5. 配置管理实现
5.1 配置热更新
Agent需要支持配置的动态更新,无需重启即可生效。我们采用观察者模式实现这一功能:
java复制public class ConfigManager {
private Map<String, String> currentConfig;
private List<ConfigListener> listeners = new CopyOnWriteArrayList<>();
public void updateConfig(Map<String, String> newConfig) {
Map<String, String> oldConfig = this.currentConfig;
this.currentConfig = newConfig;
// 找出变更的配置项
Map<String, ConfigChange> changes = new HashMap<>();
newConfig.forEach((k, v) -> {
if (!v.equals(oldConfig.get(k))) {
changes.put(k, new ConfigChange(k, oldConfig.get(k), v));
}
});
// 通知监听器
if (!changes.isEmpty()) {
listeners.forEach(l -> l.onConfigChange(changes));
}
}
public void addListener(ConfigListener listener) {
listeners.add(listener);
}
}
5.2 配置版本控制
为防止配置回滚和冲突,我们为每个配置变更分配版本号:
- FE下发配置时附带版本号
- Agent拒绝版本号小于当前版本的配置
- 配置变更记录持久化到本地文件
java复制public class VersionedConfig {
private long version;
private Map<String, String> config;
private long timestamp;
public boolean isNewerThan(VersionedConfig other) {
return this.version > other.version ||
(this.version == other.version &&
this.timestamp > other.timestamp);
}
}
6. 日志处理实现
6.1 日志收集
Agent需要收集以下日志:
- 系统日志:/var/log/messages, dmesg
- StarRocks日志:fe.log, be.INFO, be.WARNING
- Agent自身日志:agent.log
我们使用Log4j2的RollingFileAppender实现日志轮转:
xml复制<Configuration>
<Appenders>
<RollingFile name="AgentLog" fileName="logs/agent.log"
filePattern="logs/agent-%d{yyyy-MM-dd}-%i.log">
<PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss} %-5p [%t] %c{1}:%L - %m%n"/>
<Policies>
<TimeBasedTriggeringPolicy interval="1" modulate="true"/>
<SizeBasedTriggeringPolicy size="100 MB"/>
</Policies>
<DefaultRolloverStrategy max="10"/>
</RollingFile>
</Appenders>
</Configuration>
6.2 日志过滤与转发
为避免网络带宽浪费,我们需要对日志进行过滤和压缩:
java复制public class LogForwarder {
public void forwardLogs() {
List<LogEntry> logs = logCollector.collect();
List<LogEntry> filtered = logs.stream()
.filter(this::isImportant)
.collect(Collectors.toList());
if (!filtered.isEmpty()) {
byte[] compressed = compressLogs(filtered);
feClient.sendLogs(compressed);
}
}
private boolean isImportant(LogEntry log) {
return log.getLevel() >= Level.WARN ||
log.getMessage().contains("error") ||
log.getLogger().startsWith("com.starrocks");
}
private byte[] compressLogs(List<LogEntry> logs) throws IOException {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
try (GZIPOutputStream gzip = new GZIPOutputStream(baos);
ObjectOutputStream oos = new ObjectOutputStream(gzip)) {
oos.writeObject(logs);
}
return baos.toByteArray();
}
}
7. 性能优化技巧
7.1 网络通信优化
- 连接池管理:复用Thrift连接,避免频繁创建销毁
- 批量上报:将多个小请求合并为一个大请求
- 压缩传输:对大数据量启用Snappy或Gzip压缩
java复制public class FeClient {
private TTransport transport;
private TProtocol protocol;
private AgentService.Client client;
public void init() {
transport = new TFramedTransport(new TSocket("fe-host", 9020));
protocol = new TBinaryProtocol(transport);
client = new AgentService.Client(protocol);
transport.open();
}
public void sendBatchMetrics(List<Metric> metrics) {
MetricBatch batch = new MetricBatch();
batch.setMetrics(metrics);
batch.setCompressed(true);
byte[] data = Snappy.compress(serialize(batch));
client.reportMetrics(new MetricRequest(data));
}
}
7.2 内存管理优化
Agent作为常驻进程,需要特别注意内存使用:
- 对象池:重用频繁创建销毁的对象
- 大对象Off-Heap:将大缓存分配到堆外内存
- 及时释放资源:使用try-with-resources确保资源释放
java复制public class MemoryPool {
private static final int MAX_POOL_SIZE = 100;
private Queue<ByteBuffer> bufferPool = new ConcurrentLinkedQueue<>();
public ByteBuffer borrowBuffer(int size) {
ByteBuffer buffer = bufferPool.poll();
if (buffer == null || buffer.capacity() < size) {
return ByteBuffer.allocateDirect(size);
}
buffer.clear();
return buffer;
}
public void returnBuffer(ByteBuffer buffer) {
if (bufferPool.size() < MAX_POOL_SIZE) {
bufferPool.offer(buffer);
}
}
}
8. 故障处理与容错
8.1 故障检测
Agent需要能够检测并恢复以下常见故障:
- FE不可达:尝试连接其他FE节点
- 磁盘满:清理旧日志和临时文件
- 内存泄漏:达到阈值后重启自身
java复制public class HealthChecker {
public void check() {
if (isDiskFull()) {
cleanupDisk();
}
if (isMemoryHigh()) {
restartSelf();
}
}
private boolean isDiskFull() {
FileStore store = Files.getFileStore(Paths.get("/"));
return store.getUsableSpace() < store.getTotalSpace() * 0.05;
}
private void cleanupDisk() {
// 清理旧日志
FileUtils.deleteOldFiles("logs", 7);
// 清理临时文件
FileUtils.cleanDirectory("temp");
}
}
8.2 优雅降级
在资源紧张时,Agent应优先保障核心功能:
- 心跳:最高优先级,必须保证
- 任务执行:根据资源情况限制并发数
- 日志收集:可临时关闭详细日志
java复制public class ResourceAwareExecutor {
private int currentLoadLevel = 0; // 0-正常, 1-警告, 2-严重
public void execute(Task task) {
if (currentLoadLevel >= 2 && !task.isCritical()) {
throw new ResourceNotEnoughException();
}
if (currentLoadLevel >= 1) {
task.setLowPriority(true);
}
taskExecutor.execute(task);
}
public void updateLoadStatus(SystemStats stats) {
if (stats.getCpuUsage() > 90 || stats.getMemUsage() > 90) {
currentLoadLevel = 2;
} else if (stats.getCpuUsage() > 70 || stats.getMemUsage() > 70) {
currentLoadLevel = 1;
} else {
currentLoadLevel = 0;
}
}
}
9. 部署与运维实践
9.1 打包与部署
推荐使用RPM/DEB包方式部署Agent,便于版本管理和批量部署:
-
目录结构:
code复制/usr/local/starrocks-agent/ ├── bin/ # 可执行文件 ├── conf/ # 配置文件 ├── logs/ # 日志文件 ├── lib/ # 依赖库 └── plugins/ # 扩展插件 -
systemd服务单元:
ini复制[Unit] Description=StarRocks Agent After=network.target [Service] Type=simple User=starrocks Group=starrocks ExecStart=/usr/local/starrocks-agent/bin/start_agent.sh Restart=on-failure RestartSec=5 [Install] WantedBy=multi-user.target
9.2 监控与告警
Agent自身也需要被监控,关键指标包括:
- 进程存活:简单的心跳检测
- 资源使用:CPU、内存、线程数
- 功能状态:最后一次成功心跳、任务积压数
推荐监控项配置示例:
yaml复制metrics:
- name: agent_process_alive
type: gauge
help: Whether the agent process is alive
command: ps -p $(cat /var/run/starrocks-agent.pid) >/dev/null 2>&1 && echo 1 || echo 0
- name: agent_task_queue_size
type: gauge
help: Number of pending tasks in the queue
jmx: java.lang:type=Threading/ThreadCount
alerts:
- alert: AgentDown
expr: agent_process_alive == 0
for: 1m
labels:
severity: critical
annotations:
summary: "StarRocks Agent is down on {{ $labels.instance }}"
- alert: HighTaskQueue
expr: agent_task_queue_size > 100
for: 5m
labels:
severity: warning
annotations:
summary: "High task queue size on {{ $labels.instance }}"
10. 测试与验证
10.1 单元测试重点
针对Agent的核心功能应编写全面的单元测试:
- 心跳测试:模拟网络中断、FE不可用等场景
- 任务测试:验证各类任务的正确执行和资源隔离
- 配置测试:检查配置热更新的正确性
java复制public class AgentTest {
@Test
public void testHeartbeatRetry() {
FeClient mockClient = mock(FeClient.class);
when(mockClient.heartbeat(any())).thenThrow(new RuntimeException());
HeartbeatThread thread = new HeartbeatThread(mockClient);
thread.start();
// 验证重试逻辑
verify(mockClient, timeout(10000).atLeast(3)).heartbeat(any());
// 验证故障转移触发
assertTrue(thread.isFailoverTriggered());
}
}
10.2 集成测试方案
在接近生产环境的集群中验证Agent:
- 故障注入测试:模拟网络分区、磁盘满等异常
- 性能测试:测量高负载下的资源使用和稳定性
- 升级测试:验证新旧版本兼容性
测试用例表示例:
| 测试场景 | 预期结果 | 通过标准 |
|---|---|---|
| FE主节点宕机 | Agent自动切换到备用FE | 切换时间<30秒 |
| 磁盘使用率95% | Agent自动清理旧日志 | 磁盘使用率降至85%以下 |
| 并发100个任务 | 任务全部完成 | 无任务失败,CPU使用<90% |
| 配置变更 | 新配置生效且服务不中断 | 配置变更后所有功能正常 |
11. 安全加固措施
11.1 认证与加密
- 双向TLS认证:Agent与FE之间的通信加密
- 敏感配置加密:数据库密码等配置项加密存储
- 最小权限原则:Agent进程使用专用低权限用户运行
TLS配置示例:
java复制public class SecureFeClient {
public void init() throws Exception {
SSLContext sslContext = SSLContext.getInstance("TLS");
KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
KeyStore ks = KeyStore.getInstance("PKCS12");
ks.load(new FileInputStream("agent.p12"), "password".toCharArray());
kmf.init(ks, "password".toCharArray());
sslContext.init(kmf.getKeyManagers(),
getTrustManagers(),
new SecureRandom());
TSSLTransportParameters params = new TSSLTransportParameters();
params.setKeyStore("agent.p12", "password");
transport = TSSLTransportFactory.getClientSocket(
"fe-host", 9020, 5000, params);
protocol = new TBinaryProtocol(transport);
client = new AgentService.Client(protocol);
}
}
11.2 安全审计
- 操作日志:记录所有关键操作(配置变更、任务执行等)
- 访问控制:限制管理接口的访问IP
- 定期安全扫描:使用工具检查已知漏洞
审计日志示例格式:
code复制2023-07-20 14:30:45 [AUDIT] Config updated by fe-admin(10.0.0.1):
changed [query.timeout: 60->30, mem.limit: 80->70]
2023-07-20 14:35:12 [AUDIT] Task executed: taskId=12345, type=LOAD,
database=sales, table=orders, status=SUCCESS, duration=45s
12. 扩展与定制开发
12.1 插件机制
通过插件系统扩展Agent功能:
- 自定义监控指标:实现MetricPlugin接口
- 特殊任务类型:实现TaskPlugin接口
- 日志处理器:实现LogProcessor接口
插件接口定义示例:
java复制public interface MetricPlugin {
String getName();
Map<String, Object> collect();
default long getInterval() { return 5000; }
}
public class DiskIoPlugin implements MetricPlugin {
@Override
public Map<String, Object> collect() {
return DiskStats.getIoStats();
}
}
12.2 配置插件
在agent.conf中启用插件:
properties复制plugins.enabled=diskio,network,custom
plugin.diskio.class=com.starrocks.agent.plugin.DiskIoPlugin
plugin.diskio.interval=3000
plugin.custom.class=com.company.agent.plugin.CustomPlugin
13. 性能调优实战
13.1 JVM调优
针对Agent的Java进程推荐以下JVM参数:
bash复制JAVA_OPTS="-server \
-Xms4g -Xmx4g \
-XX:MaxMetaspaceSize=512m \
-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:ParallelGCThreads=4 \
-XX:ConcGCThreads=2 \
-XX:+HeapDumpOnOutOfMemoryError \
-XX:HeapDumpPath=/var/log/starrocks-agent/heapdump.hprof"
关键参数说明:
- -Xms/-Xmx:设置堆内存初始和最大值,建议相同避免动态调整开销
- UseG1GC:G1垃圾收集器适合大内存、低延迟场景
- MaxGCPauseMillis:控制GC最大停顿时间
- HeapDumpOnOutOfMemoryError:OOM时自动生成堆转储便于分析
13.2 线程池优化
根据任务类型配置不同的线程池:
- 心跳线程池:单线程,高优先级
- 任务执行线程池:根据CPU核心数配置
- 日志处理线程池:IO密集型,可配置较多线程
java复制public class ThreadPoolManager {
private ExecutorService heartbeatExecutor =
Executors.newSingleThreadExecutor(new NamedThreadFactory("heartbeat"));
private ExecutorService taskExecutor =
new ThreadPoolExecutor(
Runtime.getRuntime().availableProcessors(),
Runtime.getRuntime().availableProcessors() * 2,
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new NamedThreadFactory("task"));
private ExecutorService logExecutor =
new ThreadPoolExecutor(
4,
16,
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(5000),
new NamedThreadFactory("log"));
}
14. 常见问题排查
14.1 问题诊断流程
当Agent出现异常时,建议按以下步骤排查:
-
检查基础状态:
- 进程是否运行:
ps -ef | grep starrocks-agent - 端口是否监听:
netstat -tulnp | grep java - 资源使用情况:
top -p <pid>
- 进程是否运行:
-
查看日志:
- Agent主日志:
tail -f /var/log/starrocks-agent/agent.log - GC日志:
grep "GC pause" /var/log/starrocks-agent/gc.log
- Agent主日志:
-
验证网络连接:
- 到FE的网络:
telnet <fe-host> 9020 - 防火墙规则:
iptables -L -n
- 到FE的网络:
14.2 典型问题与解决方案
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 心跳超时 | 网络延迟或FE负载高 | 调整心跳超时参数,增加重试次数 |
| 任务积压 | 任务执行线程不足 | 增加任务线程池大小,优化任务执行逻辑 |
| 内存持续增长 | 内存泄漏 | 分析堆转储,检查缓存实现 |
| CPU使用率高 | 死循环或频繁GC | 线程转储分析,调整JVM参数 |
| 磁盘IO高 | 日志滚动或临时文件过多 | 优化日志配置,定期清理临时文件 |
15. 版本升级策略
15.1 滚动升级方案
对于生产环境,建议采用滚动升级:
- 从集群中摘除待升级节点
- 停止Agent服务:
systemctl stop starrocks-agent - 备份配置和日志
- 安装新版本RPM/DEB包
- 启动服务:
systemctl start starrocks-agent - 验证功能正常后重新加入集群
15.2 兼容性考虑
升级时需注意:
- 协议兼容性:新版本Agent应兼容旧版本FE
- 配置兼容性:旧配置应能平滑迁移到新版本
- 数据兼容性:临时文件和状态文件应能正确读取
建议先在测试环境验证升级过程,特别是跨大版本升级时。