kafka服务端--启动入口分析
一, 概述
kafka的broker节点大致包涵了一下模块
- zk连接器:KafkaZkClient
- 动态配置管理:dynamicConfigManager,dynamicConfigHandlers,这个是为了解决修改配置需要重启的问题,提取了部分配置,并不是所有配置都可以不重启生效。依赖了zkClient
- 定时调度器:kafkaScheduler,该调度器是全局共享的。 承担了logManager模块中的日志归档,日志刷新、检查点恢复,日志的offset检查,过期日志的删除。ReplicaManager模块中的高水位检查,ISR过期检查,ISR变更处理,空闲ISR停止
- 配额管理:quotaManagers ,限制了客户端生产、消费者消费、副本复制的流控。
- 日志管理服务:logManager,logDirFailureChannel ,主要是本地日志的生成,清理,滚动。依赖了kafkaScheduler,KafkaZkClient
- 元数据管理:metadataCache ,顾名思义就是针对topic元数据的缓存
- RPC服务:socketServer,接口RPC请求的服务器。连接管理,请求分发和响应消息返回。新增了单IP的请求限制及溢出水位控制。
- 副本管理:replicaManager ,副本的追加日志、删除日志,ISR的各种状态变更检查。依赖了KafkaZkClient,kafkaScheduler,logManager,quotaManagers,metadataCache。
- 请求认证管理:tokenManager ,这个是权限相关,如果不开启的话,一般不用。
- 控制器:kafkaController,这个是非常核心的,全集群唯一的控制器,控制所有的元数据变更管理,具体内容单章来说明。依赖了tokenManager,KafkaZkClient
- 管理命令服务:adminManager,针对管理员命令的服务处理,命令行方式的创建topic,删除topic,增加分区等。依赖了 metadataCache,KafkaZkClient模块
- 消费者协调者:groupCoordinator ,每个消费组,在服务都需要一个全局的消费组协调者,解决消费者的rebalance问题。依赖了replicaManager,KafkaZkClient模块
- 事务协调者:transactionCoordinator,数据提交写入到本地,到复制到其他节点等全部环节中的事务管理器。依赖了replicaManager,metadataCache。
- 请求处理线程池:requestHandlerPool , 处理来之socketServer中的请求,再调用KafkaApis处理具体的数据。依赖了socketServer,apis。
- 业务处理服务:KafkaApis,所有请求的处理入口。依赖了socketServer,replicaManager,adminManager,groupCoordinator,transactionCoordinator,fetchManager,tokenManager.可以说这个类是整个集群的统一处理的地方。
整个服务的状态流程是通过BrokerState类来控制。
启动流转过程是:NotRunning -> Starting -> RecoveringFromUncleanShutdown(非必要) -> RunningAsBroker .
停止流转过程是:RunningAsBroker -> BrokerShuttingDown -> PendingControlledShutdown -> NotRunning
服务的启动:startup
服务的停止:shutdown
其他方法都是延伸自这两个方法
startup顺序
- 状态 NotRunning -> Starting
- initZkClient //初始化Z看
- getOrGenerateClusterId //在ZK 上初始化clusterID
- getBrokerIdAndOfflineDirs //生成brokerID
- KafkaScheduler.startup //初始化全局定时器
- quotaManagers = QuotaFactory.instantiate //初始化配额管理服务
- logManager.startup() //初始化日志管理服务
- 状态 Starting -> RecoveringFromUncleanShutdown // 如果需要
- new MetadataCache //新建元数据缓存
- new SocketServer //新建RPC服务
- createReplicaManager //创建副本管理服务
- kafkaController.startup() //初始化控制器服务
- new AdminManager //新建管理员命令服务
- groupCoordinator.startup() //消费者组协调者器
- transactionCoordinator.startup() //事务协调者
- new KafkaApis //新建请求服务类
- new KafkaRequestHandlerPool //RPCServer请求的处理线程
- dynamicConfigHandlers //动态配置处理类
- new DynamicConfigManager //动态配置管理
- socketServer.startProcessors() //启动RPC服务
- 状态 Starting/RecoveringFromUncleanShutdown -> RunningAsBroker
shutdown顺序
- 状态 RunningAsBroker -> BrokerShuttingDown
- dynamicConfigManager.shutdown()
- socketServer.stopProcessingRequests()
- requestHandlerPool.shutdown()
- kafkaScheduler.shutdown()
- apis.close()
- adminManager.shutdown()
- transactionCoordinator.shutdown()
- groupCoordinator.shutdown()
- tokenManager.shutdown()
- replicaManager.shutdown()
- adminManager.shutdown()
- groupCoordinator.shutdown()
- logManager.shutdown()
- logManager.shutdown()
- zkUtils.close()
- metrics.close()
- 状态 BrokerShuttingDown -> NotRunning
总结的说:
日志管理服务logManager和副本管理replicaManager共享了同一定时器kafkaScheduler,transactionCoordinator自己独立维护了一个定时器,初始线程数时1.
对于服务的停止,broker使用了一个CountDownLatch来做标示
二, 源码分析
平常我们通过命令 kafka-server-start /usr/local/etc/kafka/server.properties
启动,kafka的启动类是Kafka.scala,最终会调用Kafka.scala类的main方法。
另外,启动脚本中还会设置相关JVM参数,如log4j配置文件地址、JVM堆大小等等
下面通过源码简单分析下kafka的启动流程以及shutdown的实现。源码分析本节都以0.10.2
版本为准
启动入口
//Kafka.scala
def main(args: Array[String]): Unit = {
try {
//根据命令行的参数,获取配置文件中的相关配置,这里获取到的也就是/usr/local/etc/kafka/server.properties的内容
//这里同时还会检查是否有入参,如果没有就会报错
val serverProps = getPropsFromArgs(args)
//根据配置构造一个kafkaServerStartable对象,这里面会检验必要的参数是否有值
val kafkaServerStartable = KafkaServerStartable.fromProps(serverProps)
//绑定一个进程关闭的钩子
Runtime.getRuntime().addShutdownHook(new Thread() {
override def run() = {
kafkaServerStartable.shutdown
}
})
//在KafkaServerStartable.scala的startup方法中,会继续调用KafkaServer#startup()方法
//在KafkaServer#startup()方法中,开始初始化并加载各个组件
kafkaServerStartable.startup
//阻塞直到kafka被关闭
//底层用了java的CountDownLatch.await()。当kafka被关闭时,对应的CountDownLatch.countDown()方法会被调用,这时候程序就会真正退出
kafkaServerStartable.awaitShutdown
}
catch {
case e: Throwable =>
fatal(e)
System.exit(1)
}
System.exit(0)
}
broker的生命周期
kafka的生命周期中的各个状态如下图:
*
* +-----------+
* |Not Running|
* +-----+-----+
* |
* v
* +-----+-----+
* |Starting +--+
* +-----+-----+ | +----+------------+
* | +>+RecoveringFrom |
* v |UncleanShutdown |
* +-------+-------+ +-------+---------+
* |RunningAsBroker| |
* +-------+-------+<-----------+
* |
* v
* +-----+------------+
* |PendingControlled |
* |Shutdown |
* +-----+------------+
* |
* v
* +-----+----------+
* |BrokerShutting |
* |Down |
* +-----+----------+
* |
* v
* +-----+-----+
* |Not Running|
* +-----------+
*
- NOT Running : 未运行状态
- Starting:正在启动中
- RunningAsBroker: broker在运行中
- RecoveringFromUncleanShutdown:从上次不完整的关闭中恢复状态,这个状态和logManager有关
- PendingControlledShutdown:broker向controller报告关闭,等待controller应答中
- BrokerShuttingDown:broker在关闭中
sever启动流程
//KafkaServer#startup()
def startup() {
try {
info("starting")
if (isShuttingDown.get)
throw new IllegalStateException("Kafka server is still shutting down, cannot re-start!")
if (startupComplete.get)
return
val canStartup = isStartingUp.compareAndSet(false, true)
if (canStartup) {
//设置broker状态为Starting
brokerState.newState(Starting)
//启动一个定时任务的线程池
kafkaScheduler.startup()
//初始化zk组件,后续用于监听、获取zk数据用
zkUtils = initZk()
//获取集群的id,如果当前集群尚未生成集群id,那就生成一个,对应zk的 /cluster/id 的值
_clusterId = getOrGenerateClusterId(zkUtils)
info(s"Cluster ID = $clusterId")
//获取或者生成一个brokerId
config.brokerId = getBrokerId
this.logIdent = "[Kafka Server " + config.brokerId + "], "
//创建一个用于度量的组件
val reporters = config.getConfiguredInstances(KafkaConfig.MetricReporterClassesProp, classOf[MetricsReporter],
Map[String, AnyRef](KafkaConfig.BrokerIdProp -> (config.brokerId.toString)).asJava)
reporters.add(new JmxReporter(jmxPrefix))
val metricConfig = KafkaServer.metricConfig(config)
metrics = new Metrics(metricConfig, reporters, time, true)
quotaManagers = QuotaFactory.instantiate(config, metrics, time)
notifyClusterListeners(kafkaMetricsReporters ++ reporters.asScala)
//创建日志管理组件,创建时会检查log目录下是否有.kafka_cleanshutdown文件,如果没有的话,broker进入RecoveringFrom UncleanShutdown 状态
logManager = createLogManager(zkUtils.zkClient, brokerState)
logManager.startup()
//创建元数据管理组件
metadataCache = new MetadataCache(config.brokerId)
//创建凭证提供者组件
credentialProvider = new CredentialProvider(config.saslEnabledMechanisms)
//创建一个sockerServer组件,并启动。该组件启动后,就会开始接收rpc请求了
socketServer = new SocketServer(config, metrics, time, credentialProvider)
socketServer.startup()
//创建一个副本管理组件,并启动该组件
replicaManager = new ReplicaManager(config, metrics, time, zkUtils, kafkaScheduler, logManager,
isShuttingDown, quotaManagers.follower)
replicaManager.startup()
//创建kafka控制器,并启动。该控制器启动后broker会尝试去zk创建节点竞争成为controller
kafkaController = new KafkaController(config, zkUtils, brokerState, time, metrics, threadNamePrefix)
kafkaController.startup()
//创建一个集群管理组件
adminManager = new AdminManager(config, metrics, metadataCache, zkUtils)
//创建群组协调器,并且启动
groupCoordinator = GroupCoordinator(config, zkUtils, replicaManager, Time.SYSTEM)
groupCoordinator.startup()
//构造授权器
authorizer = Option(config.authorizerClassName).filter(_.nonEmpty).map { authorizerClassName =>
val authZ = CoreUtils.createObject[Authorizer](authorizerClassName)
authZ.configure(config.originals())
authZ
}
//构造api组件,针对各个接口会处理不同的业务
apis = new KafkaApis(socketServer.requestChannel, replicaManager, adminManager, groupCoordinator,
kafkaController, zkUtils, config.brokerId, config, metadataCache, metrics, authorizer, quotaManagers,
clusterId, time)
//请求处理池
requestHandlerPool = new KafkaRequestHandlerPool(config.brokerId, socketServer.requestChannel, apis, time,
config.numIoThreads)
Mx4jLoader.maybeLoad()
//动态配置处理器的相关配置
dynamicConfigHandlers = Map[String, ConfigHandler](ConfigType.Topic -> new TopicConfigHandler(logManager, config, quotaManagers),
ConfigType.Client -> new ClientIdConfigHandler(quotaManagers),
ConfigType.User -> new UserConfigHandler(quotaManagers, credentialProvider),
ConfigType.Broker -> new BrokerConfigHandler(config, quotaManagers))
//初始化动态配置管理器,并启动
dynamicConfigManager = new DynamicConfigManager(zkUtils, dynamicConfigHandlers)
dynamicConfigManager.startup()
/* tell everyone we are alive */
val listeners = config.advertisedListeners.map { endpoint =>
if (endpoint.port == 0)
endpoint.copy(port = socketServer.boundPort(endpoint.listenerName))
else
endpoint
}
//kafka健康检查组件
kafkaHealthcheck = new KafkaHealthcheck(config.brokerId, listeners, zkUtils, config.rack,
config.interBrokerProtocolVersion)
kafkaHealthcheck.startup()
//记录一下恢复点
checkpointBrokerId(config.brokerId)
/* register broker metrics */
registerStats()
//broker进入RunningAsBroker状态
brokerState.newState(RunningAsBroker)
shutdownLatch = new CountDownLatch(1)
startupComplete.set(true)
isStartingUp.set(false)
AppInfoParser.registerAppInfo(jmxPrefix, config.brokerId.toString)
info("started")
}
}
catch {
case e: Throwable =>
fatal("Fatal error during KafkaServer startup. Prepare to shutdown", e)
isStartingUp.set(false)
shutdown()
throw e
}
}
启动流程就是初始化一堆组件,然后该启动的启动。这些组件以后我会一个一个介绍,现在大家先简单了解一下它们的启动过程就好了。
shutdown实现
在启动代码那里,可以看到kafka在那里面已经做了shutdown的钩子。当kafka关闭的是会执行kafkaServerStartable#shutdown()方法。最后调用了KafkaServer#shutdown()方法
def shutdown() {
try {
info("shutting down")
if (isStartingUp.get)
throw new IllegalStateException("Kafka server is still starting up, cannot shut down!")
// To ensure correct behavior under concurrent calls, we need to check `shutdownLatch` first since it gets updated
// last in the `if` block. If the order is reversed, we could shutdown twice or leave `isShuttingDown` set to
// `true` at the end of this method.
if (shutdownLatch.getCount > 0 && isShuttingDown.compareAndSet(false, true)) {
//controlledShutdown()里面会通知controller自己关闭了,会一直阻塞到通知成功,这时候broker会进入PendingControlled Shutdown状态
CoreUtils.swallow(controlledShutdown())
//broker进入BrokerShutting Down状态
brokerState.newState(BrokerShuttingDown)
if (socketServer != null)
CoreUtils.swallow(socketServer.shutdown())
if (requestHandlerPool != null)
CoreUtils.swallow(requestHandlerPool.shutdown())
CoreUtils.swallow(kafkaScheduler.shutdown())
if (apis != null)
CoreUtils.swallow(apis.close())
CoreUtils.swallow(authorizer.foreach(_.close()))
if (replicaManager != null)
CoreUtils.swallow(replicaManager.shutdown())
if (adminManager != null)
CoreUtils.swallow(adminManager.shutdown())
if (groupCoordinator != null)
CoreUtils.swallow(groupCoordinator.shutdown())
if (logManager != null)
CoreUtils.swallow(logManager.shutdown())
if (kafkaController != null)
CoreUtils.swallow(kafkaController.shutdown())
if (zkUtils != null)
CoreUtils.swallow(zkUtils.close())
if (metrics != null)
CoreUtils.swallow(metrics.close())
//broker进入Not Running状态
brokerState.newState(NotRunning)
startupComplete.set(false)
isShuttingDown.set(false)
CoreUtils.swallow(AppInfoParser.unregisterAppInfo(jmxPrefix, config.brokerId.toString))
shutdownLatch.countDown()
info("shut down completed")
}
}
catch {
case e: Throwable =>
fatal("Fatal error during KafkaServer shutdown.", e)
isShuttingDown.set(false)
throw e
}
}
做的事情就是将之前的那些组件优雅的关闭掉。在关闭这些组件前,可能还会去通知controller自己关闭了,收到controller答复后再继续关闭剩下的组件。
向controller报告关闭
如果 controlled.shutdown.enable 开启的话,broker在关闭时会向controller报告自己关闭,这样controller可以对broker的下线及时做一些操作,比如partition的重新选举、分区副本的关闭、通知其他的broker元数据变动等。该配置默认是开启的。
broker向controller报告关闭的流程是这样的:
- 检查controlled.shutdown.enable是否开启,如果开启,broker就进入PendingControlledShutdown状态,同时开始向controller发送shutdown请求
- 检查重试次数是否达到controlled.shutdown.max.retries配置的值,如果是的话则退出不在发送shutdown请求。重试次数默认是3次
- 从zk获取当前controller所在的id,然后向该节点发送shutdown请求
- 如果请求失败,休眠controlled.shutdown.retry.backoff.ms毫秒后,重试次数+1,重新进入步骤二
- 如果请求成功,说明controller接受到了shutdown请求,并处理完成。