[재 인쇄] 도로 2 년 FLINK 마이그레이션 : 독립형에서 실에 5 회 처리 능력 업그레이드에

2 년 동안 도로의 FLINK 마이그레이션 : 독립형에서 실에 5 회 처리 능력 업그레이드에

HTTPS : // segmentfault.com/a/1190000020209179

 

6

I. 배경과 고통 포인트

TD-ETL 프레임 워크의 자체 연구를 사용하여 프레임을 스트리밍 2017 년 상반기,이 개 게임 분석 및 앱 웹 로그 분석 제품의 TalkingData,합니다. 이 프레임 워크는 체인 체인저가 될 수 있습니다 구현해야하는 다른 작업에 대한 스트리밍 작업을 개발의 복잡성을 줄이고, 지원 수준은 확장 된 성능을 한 번 만나 비즈니스 요구에, 허용됩니다.

그러나 2016 년 말과 2017 년 상반기, 우리는이 프레임 워크의 다음과 같은 중요한 제한을 발견 :

  1. 성능 위험 : 앱 분석-ETL 어댑터 및 게임 분석 - ETL 어댑터이 두 모듈이 지연 인덱스 계산되었다 심각한 성능 문제 (전체 GC)에서 휴일에왔다;
  2. 내결함성 프레임 워크의 부족 : 카프카 또는 ZK의 오프셋에 따라 저장 만에-최소 한 번에 도달 할 수 있으며, 필요 정확히-한 번 달성하기 위해 다른 서비스와 스토리지에 의존하고, 비정상적인 다시 시작 손실 계수로 이어질 수있다;
  3. 부적절한 기술 프레임 워크 : 수있는 DAG의 완료되지 표현, 복잡한 문제에 대한 여러 스트리밍 서비스가 문제를 해결하기 위해 함께 워크의 조합의 수에 따라 달라 요구한다

TalkingData 두 제품은 주로 최근 몇 년 동안 사업의 볼륨 확장을 계속, 당신은 더 많은 우리의 스트리밍을 확대하기 위해 엔진을 스트리밍 전체가 기능을 갖춘 강력한 성능을 선택해야합니다, 다양한 모바일 앱에 데이터 분석 서비스를 제공하고 게임을 종료 서비스를 제공합니다. 2016 년 말부터 주로 FLINK, 헤론, 스트리밍 스파크에서 선택을 위해, 연구하기 시작했다.

결국, 우리는 다음과 같은 고려 사항에 따라, FLINK를 선택 :

  1. 내결함성, 정확히-번에 대한 지원을 개선 FLINK;
  2. FLINK 사용자 정의 연산자도 더 편리하고 DAG를 완료 할 수 있습니다 분할의 완전한 스트림에 API를 호출하고 직접적인 표현을 가입 할 수 있습니다, 풍부한 스트리밍 연산자를 통합했다;
  3. 일부 서비스 전체-GC 어느 정도 현재의 ETL 프레임 워크의 문제를 방지 할 수 있으며, JVM에 완전히 의존하지 않고 독립적 인 메모리 관리를 FLINK;
  4. 한 - 일 게임에서 GA 주파수 분포가 수업 기간 문제 지표의 분포와 유사한 경우 FLINK의 창 메커니즘은 오랫동안 게임을 해결하기;
  5. 시간 가장 진보 된 스트리밍 프레임 워크에서 FLINK 아이디어 : 특별한 경우로 배치 스트림, 궁극적으로 승인 균일 한 흐름;

둘째, 진화 경로

2.1 독립 클러스터 (1.1.3-> 1.1.5-> 1.3.2)

우리는 독립 클러스터 모델을 구축하기 시작했다. 2017 년 상반기부터는 점차 게임 웹 로그 분석 몇 가지 작은 트래픽 ETL-작업 FLINK 마이그레이션, 4 월에 제품이 SDK의 각 버전에 대한 ETL-작업 데이터를 받고있다 완전히 FLINK으로 마이그레이션 및 통합 될 일. 다음 데이터 스트림 및 스트림 그래프 형성 :

clipboard.png

데이터는도 흐름 그래프. 1. 게임 분석-ETL 어댑터에 마이그레이션을 FLINK

clipboard.png

스트림 그래프 그림 2. 게임 분석 - ETL

상기 데이터 액세스 로직 외부 저장 메모리 액세스 로직의 복잡도를 고려할 필요가 없다, FLINK 현장에서 ETL 서비스로 추상화되어 그래서, 보행 통해 통화 ETL 서비스로도, FLINK-작업 흐름 및 업무의 자기 캐시, 그래서 일반적인 서비스의 완성뿐만 아니라 모두 작업 자체 GC 압력을 줄일 수있다.

이 FLINK의 1.1.3 버전이 낮은 수준의 API의 Kafka08를 사용하여 카프카 커넥터의 카프카 오프셋하지 않았기 때문에 미터 이하의 ZK하고는 소비가 제출 한 모니터링을 제공했기 때문에 또한, 우리는 서비스에서 모니터를 구성 그러므로 우리는 순간 속도, 침전물과 FLINK의 기타 소비자 메트릭 작업 및 기업 올빼미 모니터 경보에 대한 완전한 액세스의 활동을 감시하는 모니터를 구축 할 필요가있다.

이 때, FLINK의 독립형 클러스터 억에 대한 메시지 일 당 12TB의 총 처리량의 평균 처리, 게임 분석의 모든 트래픽을 수행하고있다. 로그의 일일 평균 볼륨이 18 억 하루 상승 여름에, 20TB 약의 일상 처리량은 30,000의 피크를 TPS.

이 과정에서, 우리는 온라인 소비자 퇴적물에 의한, 성공적으로 다시 시작할 수없는 자동 재시작 작업 후 아무 이유없이 자동 다시 시작 클러스터링, 다른 문제 배포 균형되지 않은 독립형 클러스터 작업에 FLINK 소비가 균형되지 않는 일을, 발생했습니다. (우리는 이러한 문제 제 3 장에서 자세히 설명하고 성능은 일반적인 솔루션이다.)

여름 휴가 후, 우리는 FLINK 테스트를 서서 생각 때문에 앱 웹 로그 분석 ETL-작업의 시작은 FLINK로 마이그레이션. 아래의 데이터 흐름도 형성 :

clipboard.png

데이터 처리 SDK 표준도 3 응용 분석-ETL 어댑터 마이그레이션 그래프 FLINK 흐름

clipboard.png

의도. 4. 앱 분석-ETL-FLINK 작업 스트림의 그래프

년 3 월 2017 년 개시 많은 수의 사용자가 통합 된 JSON SDK로 이전하기 시작, 새로운 SDK 피크의 카프카 항목은 /는 3W의 마지막에 s를 8K로부터 흘러 / S. 이 시점에서, 전체 FLINK 독립형 클러스터는 네 개의 작업이 제품의 총을 배치, 일 평균 처리량은 35TB에 도달했다.

그럼 난이 매우 심각한 문제를 만났다 :

1) 작업의 독립형 클러스터 선점 리소스 및 독립 클러스터 모드에서만 작업 슬롯으로 만 힙 메모리의 자원 분리 작업 관리자에서 수행 할 수 있습니다. 그것은 때때로 문제 게임 웹 로그 분석 라인 퇴적물 원인이 앱 웹 로그 분석 트래픽 흐름 라인으로 이어질 것입니다 자원의 불규칙적 분포에 의해 발생했을 방식으로 독립 클러스터 앞서 언급 한 FLINK의 배포 작업으로 인해 동시에;

2) 我们的source operator的并行度等同于所消费Kafka topic的partition数量,而中间做etl的operator的并行度往往会远大于Kafka的partition数量。因此最后的job graph不可能完全被链成一条operator chain,operator之间的数据传输必须通过Flink的network buffer的申请和释放,而1.1.x 版本的network buffer在数据量大的时候很容易在其申请和释放时造成死锁,而导致Flink明明有许多消息要处理,但是大部分线程处于waiting的状态导致业务的大量延迟。

这些问题逼迫着我们不得不将两款产品的job拆分到两个standalone cluster中,并对Flink做一次较大的版本升级,从1.1.3(中间过度到1.1.5)升级成1.3.2。最终升级至1.3.2在18年的Q1完成,1.3.2版本引入了增量式的checkpoint提交并且在性能和稳定性上比1.1.x版本做了巨大的改进。升级之后,Flink集群基本稳定,尽管还有消费不均匀等问题,但是基本可以在业务量增加时通过扩容机器来解决。

2.2 Flink on yarn (1.7.1)

因为standalone cluster的资源隔离做的并不优秀,而且还有deploy job不均衡等问题,加上社区上使用Flink on yarn已经非常成熟,因此我们在18年的Q4就开始计划将Flink的standalone cluster迁移至Flink on yarn上,并且Flink在最近的版本中对于batch的提升较多,我们还规划逐步使用Flink来逐步替换现在的批处理引擎。

clipboard.png

图5. Flink on yarn cluster规划

如图5,未来的Flink on yarn cluster将可以完成流式计算和批处理计算,集群的使用者可以通过一个构建service来完成stream/batch job的构建、优化和提交,job提交后,根据使用者所在的业务团队及服务客户的业务量分发到不同的yarn队列中,此外,集群需要一个完善的监控系统,采集用户的提交记录、各个队列的流量及负载、各个job的运行时指标等等,并接入公司的OWL。

从19年的Q1开始,我们将App Analytics的部分stream job迁移到了Flink on yarn 1.7中,又在19年Q2前完成了App Analytics所有处理统一JSON SDK的流任务迁移。当前的Flink on yarn集群的峰值处理的消息量达到30W/s,日均日志吞吐量达约到50亿条,约60TB。在Flink迁移到on yarn之后,因为版本的升级性能有所提升,且job之间的资源隔离确实优于standalone cluster。迁移后我们使用Prometheus+Grafana的监控方案,监控更方便和直观。

我们将在后续将Game Analytics的Flink job和日志导出的job也迁移至该on yarn集群,预计可以节约1/4的机器资源。

三、重点问题的描述与解决

在Flink实践的过程中,我们一路上遇到了不少坑,我们挑出其中几个重点坑做简要讲解。

1.少用静态变量及job cancel时合理释放资源

在我们实现Flink的operator的function时,一般都可以继承AbstractRichFunction,其已提供生命周期方法open()/close(),所以operator依赖的资源的初始化和释放应该通过重写这些方法执行。当我们初始化一些资源,如spring context、dubbo config时,应该尽可能使用单例对象持有这些资源且(在一个TaskManager中)只初始化1次,同样的,我们在close方法中应当(在一个TaskManager中)只释放一次。

static的变量应该慎重使用,否则很容易引起job cancel而相应的资源没有释放进而导致job重启遇到问题。规避static变量来初始化可以使用org.apache.flink.configuration.Configuration(1.3)或者org.apache.flink.api.java.utils.ParameterTool(1.7)来保存我们的资源配置,然后通过ExecutionEnvironment来存放(Job提交时)和获取这些配置(Job运行时)。

示例代码:

Flink 1.3
设置及注册配置

StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment();
Configuration parameters = new Configuration(); parameters.setString("zkConnects", zkConnects); parameters.setBoolean("debug", debug); env.getConfig().setGlobalJobParameters(parameters); 

获取配置(在operator的open方法中)

@Override
public void open(Configuration parameters) throws Exception { super.open(parameters); ExecutionConfig.GlobalJobParameters globalParams = getRuntimeContext().getExecutionConfig().getGlobalJobParameters(); Configuration globConf = (Configuration) globalParams; debug = globConf.getBoolean("debug", false); String zks = globConf.getString("zkConnects", ""); //.. do more .. } 

Flink 1.7
设置及注册配置

ParameterTool parameters = ParameterTool.fromArgs(args);

// set up the execution environment
final ExecutionEnvironment env = ExecutionEnvironment.getExecutionEnvironment(); env.getConfig().setGlobalJobParameters(parameters); 

获取配置

public static final class Tokenizer extends RichFlatMapFunction<String, Tuple2<String, Integer>> { @Override public void flatMap(String value, Collector<Tuple2<String, Integer>> out) { ParameterTool parameters = (ParameterTool) getRuntimeContext().getExecutionConfig().getGlobalJobParameters(); parameters.getRequired("input"); // .. do more .. 

2.NetworkBuffer及operator chain

如前文所述,当Flink的job 的上下游Task(的subTask)分布在不同的TaskManager节点上时(也就是上下游operator没有chained在一起,且相对应的subTask分布在了不同的TaskManager节点上),就需要在operator的数据传递时申请和释放network buffer并通过网络I/O传递数据。

其过程简述如下:上游的operator产生的结果会通过RecordWriter序列化,然后申请BufferPool中的Buffer并将序列化后的结果写入Buffer,此后Buffer会被加入ResultPartition的ResultSubPartition中。ResultSubPartition中的Buffer会通过Netty传输至下一级的operator的InputGate的InputChannel中,同样的,Buffer进入InputChannel前同样需要到下一级operator所在的TaskManager的BufferPool申请,RecordReader读取Buffer并将其中的数据反序列化。BufferPool是有限的,在BufferPool为空时RecordWriter/RecordReader所在的线程会在申请Buffer的过程中wait一段时间,具体原理可以参考:[1], [2]。

简要截图如下:

clipboard.png

图6. Flink的网络栈, 其中RP为ResultPartition、RS为ResultSubPartition、IG为InputGate、IC为inputChannel。

在使用Flink 1.1.x和1.3.x版本时,如果我们的network buffer的数量配置的不充足且数据的吞吐量变大的时候,就会遇到如下现象:

clipboard.png

图7. 上游operator阻塞在获取network buffer的requestBuffer()方法中

clipboard.png

图8. 下游的operator阻塞在等待新数据输入

clipboard.png

图9. 下游的operator阻塞在等待新数据输入

我们的工作线程(RecordWriter和RecordReader所在的线程)的大部分时间都花在了向BufferPool申请Buffer上,这时候CPU的使用率会剧烈的抖动,使得Job的消费速度下降,在1.1.x版本中甚至会阻塞很长的一段时间,触发整个job的背压,从而造成较严重的业务延迟。

这时候,我们就需要通过上下游operator的并行度来计算ResultPartition和InputGate中所需要的buffer的个数,以配置充足的taskmanager.network.numberOfBuffers。

clipboard.png

图10. 不同的network buffer对CPU使用率的影响

当配置了充足的network buffer数时,CPU抖动可以减少,Job消费速度有所提高。

在Flink 1.5之后,在其network stack中引入了基于信用度的流量传输控制(credit-based flow control)机制[2],该机制大限度的避免了在向BufferPool申请Buffer的阻塞现象,我们初步测试1.7的network stack的性能确实比1.3要高。

但这毕竟还不是最优的情况,因为如果借助network buffer来完成上下游的operator的数据传递不可以避免的要经过序列化/反序列化的过程,而且信用度的信息传递有一定的延迟性和开销,而这个过程可以通过将上下游的operator链成一条operator chain而避免。

因此我们在构建我们流任务的执行图时,应该尽可能多的让operator都chain在一起,在Kafka资源允许的情况下可以扩大Kafka的partition而使得source operator和后继的operator 链在一起,但也不能一味扩大Kafka topic的partition,应根据业务量和机器资源做好取舍。更详细的关于operator的training和task slot的调优可以参考: [4]。

3.Flink中所选用序列化器的建议

在上一节中我们知道,Flink的分布在不同节点上的Task的数据传输必须经过序列化/反序列化,因此序列化/反序列化也是影响Flink性能的一个重要因素。Flink自有一套类型体系,即Flink有自己的类型描述类(TypeInformation)。Flink希望能够掌握尽可能多的进出operator的数据类型信息,并使用TypeInformation来描述,这样做主要有以下2个原因:

  1. 类型信息知道的越多,Flink可以选取更好的序列化方式,并使得Flink对内存的使用更加高效;
  2. TypeInformation内部封装了自己的序列化器,可通过createSerializer()获取,这样可以让用户不再操心序列化框架的使用(例如如何将他们自定义的类型注册到序列化框架中,尽管用户的定制化和注册可以提高性能)。

总体上来说,Flink推荐我们在operator间传递的数据是POJOs类型,对于POJOs类型,Flink默认会使用Flink自身的PojoSerializer进行序列化,而对于Flink无法自己描述或推断的数据类型,Flink会将其识别为GenericType,并使用Kryo进行序列化。Flink在处理POJOs时更高效,此外POJOs类型会使得stream的grouping/joining/aggregating等操作变得简单,因为可以使用如:dataSet.keyBy("username") 这样的方式直接操作数据流中的数据字段。

除此之外,我们还可以做进一步的优化:
1) 显示调用returns方法,从而触发Flink的Type Hint:
dataStream.flatMap(new MyOperator()).returns(MyClass.class)
returns方法最终会调用TypeExtractor.createTypeInfo(typeClass) ,用以构建我们自定义的类型的TypeInformation。createTypeInfo方法在构建TypeInformation时,如果我们的类型满足POJOs的规则或Flink中其他的基本类型的规则,会尽可能的将我们的类型“翻译”成Flink熟知的类型如POJOs类型或其他基本类型,便于Flink自行使用更高效的序列化方式。

//org.apache.flink.api.java.typeutils.PojoTypeInfo

@Override
@PublicEvolving
@SuppressWarnings("unchecked") public TypeSerializer<T> createSerializer(ExecutionConfig config) { if (config.isForceKryoEnabled()) { return new KryoSerializer<>(getTypeClass(), config); } if (config.isForceAvroEnabled()) { return AvroUtils.getAvroUtils().createAvroSerializer(getTypeClass()); } return createPojoSerializer(config); } 

对于Flink无法“翻译”的类型,则返回GenericTypeInfo,并使用Kryo序列化:

//org.apache.flink.api.java.typeutils.TypeExtractor

@SuppressWarnings({ "unchecked", "rawtypes" })
private <OUT,IN1,IN2> TypeInformation<OUT> privateGetForClass(Class<OUT> clazz, ArrayList<Type> typeHierarchy, ParameterizedType parameterizedType, TypeInformation<IN1> in1Type, TypeInformation<IN2> in2Type) { checkNotNull(clazz); // 尝试将 clazz转换为 PrimitiveArrayTypeInfo, BasicArrayTypeInfo, ObjectArrayTypeInfo // BasicTypeInfo, PojoTypeInfo 等,具体源码已省略 //... //如果上述尝试不成功 , 则return a generic type return new GenericTypeInfo<OUT>(clazz); } 

2) 注册subtypes: 通过StreamExecutionEnvironment或ExecutionEnvironment的实例的registerType(clazz)方法注册我们的数据类及其子类、其字段的类型。如果Flink对类型知道的越多,性能会更好;

3) 如果还想做进一步的优化,Flink还允许用户注册自己定制的序列化器,手动创建自己类型的TypeInformation,具体可以参考Flink官网:[3];

在我们的实践中,最初为了扩展性,在operator之间传递的数据为JsonNode,但是我们发现性能达不到预期,因此将JsonNode改成了符合POJOs规范的类型,在1.1.x的Flink版本上直接获得了超过30%的性能提升。在我们调用了Flink的Type Hint和env.getConfig().enableForceAvro()后,性能得到进一步提升。这些方法一直沿用到了1.3.x版本。

在升级至1.7.x时,如果使用env.getConfig().enableForceAvro()这个配置,我们的代码会引起校验空字段的异常。因此我们取消了这个配置,并尝试使用Kyro进行序列化,并且注册我们的类型的所有子类到Flink的ExecutionEnvironment中,目前看性能尚可,并优于旧版本使用Avro的性能。但是最佳实践还需要经过比较和压测KryoSerializerAvroUtils.getAvroUtils().createAvroSerializerPojoSerializer才能总结出来,大家还是应该根据自己的业务场景和数据类型来合理挑选适合自己的serializer。

4.Standalone模式下job的deploy与资源隔离共享

结合我们之前的使用经验,Flink的standalone cluster在发布具体的job时,会有一定的随机性。举个例子,如果当前集群总共有2台8核的机器用以部署TaskManager,每台机器上一个TaskManager实例,每个TaskManager的TaskSlot为8,而我们的job的并行度为12,那么就有可能会出现下图的现象:

第一个TaskManager的slot全被占满,而第二个TaskManager只使用了一半的资源!资源严重不平衡,随着job处理的流量加大,一定会造成TM1上的task消费速度慢,而TM2上的task消费速度远高于TM1的task的情况。假设业务量的增长迫使我们不得不扩大job的并行度为24,并且扩容2台性能更高的机器(12核),在新的机器上,我们分别部署slot数为12的TaskManager。经过扩容后,集群的TaskSlot的占用可能会形成下图:

新扩容的配置高的机器并没有去承担更多的Task,老机器的负担仍然比较严重,资源本质上还是不均匀!

除了standalone cluster模式下job的发布策略造成不均衡的情况外,还有资源隔离差的问题。因为我们在一个cluster中往往会部署不止一个job,而这些job在每台机器上都共用JVM,自然会造成资源的竞争。起初,我们为了解决这些问题,采用了如下的解决方法:

  1. 将TaskManager的粒度变小,即一台机器部署多个实例,每个实例持有的slot数较少;
  2. 将大的业务job隔离到不同的集群上。

这些解决方法增加了实例数和集群数,进而增加了维护成本。因此我们决定要迁移到on yarn上,目前看Flink on yarn的资源分配和资源隔离确实比standalone模式要优秀一些。

四、总结与展望

Flink在2016年时仅为星星之火,而只用短短两年的时间就成长为了当前最为炙手可热的流处理平台,而且大有统一批与流之势。经过两年的实践,Flink已经证明了它能够承接TalkingData的App Analytics和Game Analytics两个产品的流处理需求。接下来我们会将更复杂的业务和批处理迁移到Flink上,完成集群部署和技术栈的统一,最终实现图5 中Flink on yarn cluster 的规划,以更少的成本来支撑更大的业务量。

参考资料:
[1] https://cwiki.apache.org/conf...
[2] https://flink.apache.org/2019...
[3] https://ci.apache.org/project...
[4] https://mp.weixin.qq.com/s/XR...

저자에 관하여 :
샤오 치앙 : TalkingData 수석 엔지니어, TalkingData 통계 분석 및 게임 분석 응용 프로그램 분석 기술 감독. 항공 우주의 북경 대학을 졸업, 주로 빅 데이터 플랫폼에 종사하는 특정 연구 컴퓨팅 및 스토리지 대류 배포됩니다.

추천

출처www.cnblogs.com/jinanxiaolaohu/p/11876627.html