Kafka配额
从0.9开始,Kafka集群能够对生产和消费设置配额。为每个客户端分组设置配额阈值(基于字节比率)。
Kafka集群有能力对请求进行配额来控制客户端使用的broker资源。可以为共享配额的每个客户组执行两种类型的客户配额:
1.通过配额定义网络带宽的字节率阈值(从0.9版本开始)
2.请求率配额将CPU的利用率阈值定义为网络和I/O线程的百分比(自0.11版本起)
为什么需要配额?
生产者和消费者的可能生产/消费非常大量的数据,从而垄断了broker的资源,引起网络饱和,配额可防止这个问题。在大型多节点集群中更加重要。其中有一小部分不良行为的用户将被降权。事实上,当kafka作为服务运行时,可以根据约定好的协议执行API限制。
配额涉及到的用户权限
Kafka配额管理所能配置的对象(或者说粒度)有3种:
user + clientid
user
clientid
这3种都是对接入的client的身份进行的认定方式。其中,clientid是每个接入kafka集群的client的一个身份标志,在ProduceRequest和FetchRequest中都需要带上;user只有在开启了身份认证的kafka集群才有。
可配置的选项包括:
producer_byte_rate。发布者单位时间(每秒)内可以发布到单台broker的字节数。
consumer_byte_rate。消费者单位时间(每秒)内可以从单台broker拉取的字节数。
为user和client-id分组定义配额配置。可根据自身需要去覆盖默认的配置,这个机制类似于topic日志配置覆盖。用户和(user, client-id)配额覆盖写在ZooKeeper的/config/users下,client-id配额覆盖写在/config/clients下。这些配置被所有broker读取,并立即生效。并且我们更改配置而无需重启整个集群。每个分组默认的配额也可使用相同的机制来动态地更新。
kafka在管理配额的时候,是以“组”的概念来管理的。而管理的对象,则是producer或consumer到broker的一条条的TCP连接。
那么在进行额度管理的时候,kafka首先需要确认,这条连接属于哪个“组”,进而确定当前连接是否超过了所属“组”的总额度。
配额配置的优先级顺序为:
- /config/users/
/clients/ - /config/users/
/clients/ - /config/users/
- /config/users/
/clients/ - /config/users/
/clients/ - /config/users/
- /config/clients/
- /config/clients/
如何配置网络带宽配额
网络带宽配额被定义为的字节速率阈值(客户端的每个分组共享的配额)。默认情况下,每个独立的客户端分组按照集群的配置接收固定的配额(字节/秒)。这个配额是基于每个broker上的定义。每个客户端分组在客户端被限制之前发布/获取每个broker的最大X字节/秒。
可以通过两种方式来作配额管理:
1.在配置文件中指定所有client-id的统一配额。
动态修改zookeeper中相关znode的值,可以配置指定client-id的配额。
2.使用第一种方式,必须重启broker,而且还不能针对特定client-id设置。所以,推荐大家使用第二种方式。
1)使用官方脚本修改配额
kafka官方的二进制包中,包含了一个脚本bin/kafka-configs.sh,支持针对user,client-id,(user,client-id)等三种纬度设置配额(也是通过修改zk来实现的)。
配置user+clientid。例如,user为”user1”,clientid为”clientA”。
bin/kafka-configs.sh --zookeeper localhost:2181 --alter --add-config 'producer_byte_rate=1024,consumer_byte_rate=2048' --entity-type users --entity-name user1 --entity-type clients --entity-name clientA
配置user。例如,user为”user1”
bin/kafka-configs.sh --zookeeper localhost:2181 --alter --add-config 'producer_byte_rate=1024,consumer_byte_rate=2048' --entity-type users --entity-name user1
配置client-id。例如,client-id为”clientA”
bin/kafka-configs.sh --zookeeper localhost:2181 --alter --add-config 'producer_byte_rate=1024,consumer_byte_rate=2048' --entity-type clients --entity-name clientA
2)直接写zk来修改配额
如果我们希望能够在代码里面直接写zk来实现配额管理的话,那要怎样操作呢?
假定我们在启动kafka时指定的zookeeper目录是kafka_rootdir。
1配置user+clientid。例如,针对”user1”,”clientA”的配额是10MB/sec,其它clientid的默认配额是5MB/sec。
znode: ${kafka_rootdir}/config/users/user1/clients/clientid; value: {"version":1,"config":{"producer_byte_rate":"10485760","consumer_byte_rate":"10485760"}}
znode: {kafka_rootdir}/config/users/user1/clients/; value: {"version":1,"config":{"producer_byte_rate":"5242880","consumer_byte_rate":"5242880"}}
配置user。例如,”user2”的配额是1MB/sec,其它user的默认配额是5MB/sec。
znode: ${kafka_rootdir}/config/users/user1; value: {"version":1,"config":{"producer_byte_rate":"1048576","consumer_byte_rate":"1048576"}}
znode: ${kafka_rootdir/config/users/; value: {"version":1,"config":{"producer_byte_rate":"5242880","consumer_byte_rate":"5242880"}}
配置client-id。例如,”clientB”的配额是2MB/sec,其它clientid的默认配额是1MB/sec。
znode:${kafka_rootdir}/config/clients/clientB'; value:{“version”:1,”config”:{“producer_byte_rate”:”2097152”,”consumer_byte_rate”:”2097152”}}
znode:${kafka_rootdir}/config/clients/; value:{“version”:1,”config”:{“producer_byte_rate”:”1048576”,”consumer_byte_rate”:”1048576”}}`
无论是使用官方的脚本工具,还是自己写zookeeper,最终都是将配置写入到zk的相应znode。所有的broker都会watch这些znode,在数据发生变更时,重新获取配额值并及时生效。为了降低配额管理的复杂度和准确度,kafka中每个broker各自管理配额。所以,上面我们配置的那些额度值都是单台broker所允许的额度值。
超额处理
如果连接超过了配额值会怎么样呢?kafka给出的处理方式是:延时回复给业务方,不使用特定返回码。
具体到producer还是consumer,处理方式又有所不同:
- Producer。如果Producer超额了,先把数据append到log文件,再计算延时时间,并在ProduceResponse的ThrottleTime字段填上延时的时间(v2,只在0.10.0版本以上支持)。
- Consumer。如果Consumer超额了,先计算延时时间,在延时到期后再去从log读取数据并返回给Consumer。否则无法起到限制对文件系统的读蜂拥。在v1(0.9.0以上版本)和v2版本的FetchResponse中有ThrottleTime字段,表示因为超过配额而延时了多久。
多线程优先级
Java线程可以有优先级的设定,高优先级的线程比低优先级的线程有更高的几率得到执行(不完全正确)。
优先级的取值
Java线程的优先级是一个整数,其取值范围是1 (Thread.MIN_PRIORITY ) - 10 (Thread.MAX_PRIORITY )。
Thread源代码里对NORM_PRIORITY (数值为5) 的注释是“线程默认的优先级”
/**
* The default priority that is assigned to a thread.
*/
public final static int NORM_PRIORITY = 5;
其实不然。默认的优先级是父线程的优先级。在init方法里,
Thread parent = currentThread();
this.priority = parent.getPriority();
或许这么解释是因为Java程序的主线程(main方法)的优先级默认是为NORM_PRIORITY,这样不主动设定优先级的,后续创建的线程的优先级也都是NORM_PRIORITY了。
设置优先级
可以通过setPriority方法(final的,不能被子类重载)更改优先级。优先级不能超出1-10的取值范围,否则抛出IllegalArgumentException。另外如果该线程已经属于一个线程组(ThreadGroup),该线程的优先级不能超过该线程组的优先级:
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}
其中setPriority0是一个本地方法。
线程组的最大优先级
我们可以设定线程组的最大优先级,当创建属于该线程组的线程时该线程的优先级不能超过这个数。
线程组最大优先级的设定:
- 系统线程组的最大优先级默认为Thread.MAX_PRIORITY
- 创建线程组的时候其最大优先级默认为父线程组(如果未指定父线程组,则其父线程组默认为当前线程所属线程组)的最大优先级
- 可以通过setMaxPriority更改最大优先级,但无法超过父线程组的最大优先级
setMaxPriority的问题:
- 该方法只能更改本线程组及其子线程组(递归)的最大优先级。
- 但不能影响已经创建的直接或间接属于该线程组的线程的优先级,也就是说,即使目前有一个子线程的优先级比新设定的线程组优先级大,也不会更改该子线程的优先级。只有当试图改变子线程的优先级或者创建新的子线程的时候,线程组的最大优先级才起作用。
线程优先级的问题
以下内容摘抄、翻译自JAVAMEX -> Java threading introduction -> Thread priorioties
对于线程优先级,我们需要注意:
- Thread.setPriority()可能根本不做任何事情,这跟你的操作系统和虚拟机版本有关
- 线程优先级对于不同的线程调度器可能有不同的含义,可能并不是你直观的推测。特别地,优先级并不一定是指CPU的分享。在UNIX系统,优先级或多或少可以认为是CPU的分配,但Windows不是这样
- 线程的优先级通常是全局的和局部的优先级设定的组合。Java的setPriority()方法只应用于局部的优先级。换句话说,你不能在整个可能的范围 内设定优先级。(这通常是一种保护的方式,你大概不希望鼠标指针的线程或者处理音频数据的线程被其它随机的用户线程所抢占)
- 不同的系统有不同的线程优先级的取值范围,但是Java定义了10个级别(1-10)。这样就有可能出现几个线程在一个操作系统里有不同的优先级,在另外一个操作系统里却有相同的优先级(并因此可能有意想不到的行为)
- 操作系统可能(并通常这么做)根据线程的优先级给线程添加一些专有的行为(例如”only give a quantum boost if the priority is below X“)。这里再重复一次,优先级的定义有部分在不同系统间有差别。
- 大多数操作系统的线程调度器实际上执行的是在战略的角度上对线程的优先级做临时操作(例如当一个线程接收到它所等待的一个事件或者I/O),通常操作系统知道最多,试图手工控制优先级可能只会干扰这个系统。
- 你的应用程序通常不知道有哪些其它进程运行的线程,所以对于整个系统来说,变更一个线程的优先级所带来的影响是难于预测的。例如你可能发现,你有一个预期 为偶尔在后台运行的低优先级的线程几乎没有运行,原因是一个病毒监控程序在一个稍微高一点的优先级(但仍然低于普通的优先级)上运行,并且无法预计你程序 的性能,它会根据你的客户使用的防病毒程序不同而不同。
一点想法
有个具体场景,需要在一个账户中先扣钱再还钱,在高并发场景下,取的钱数越大,扣到钱的概率越低,但是不允许处于等待扣钱状态,需要怎么处理?
(以下皆为个人愚见,很可能是错误的)
1.kafka配额,动态修改消费者拉取消息的速度。
kakfa是可以对生产者的消息发送指定分区(parition),一个分区只能对应一个消费者。我们按金额段来分区,当大金额段的消费者出现扣不到钱的情况,动态修改其他消费的拉取消息速度,让其慢下来(相当于一个一边放水,一边进水的水池,让其水位慢慢上升,直到可以被大金额段的消费者扣除,再恢复)
2.线程优先级,动态改变线程池的大小,具体思想和kafka配额一致。