Elasticsearch 7的服务端只要开了用户密码认证,Flink里原来照抄官方Demo就能跑通的ES连接代码,十有八九会当场翻车。401、SSL握手失败、批量写入被拒轮着来,哪怕你明明把用户名和密码写死在代码里,它也给你甩脸子。这个场景我前前后后折腾了好几个版本,也帮同事排过不少类似问题,今天干脆系统地把“Flink连接需要密码认证的Elasticsearch 7”这件事从原理到代码、从排错到上生产讲清楚。
这篇文章适合两类人:一类是刚接手Flink项目、ES集群正巧开了安全认证,任务一直报错无从下手的;另一类是已经能跑通,但担心自己配置埋了雷、想搞明白背后原理的人。我会把认证信息怎么传、SSL证书怎么放、连接器版本怎么对齐、报错怎么看这四件事串起来讲,保证每一步都有据可依。
1. 为什么ES 7开认证之后,Flink默认代码必挂
先说一个很多人忽略的背景:Elasticsearch 7.x的大部分开源连接器,默认设计前提是“内网可信环境”。集群开了xpack.security.enabled: true之后,ES的HTTP层会对所有请求做身份校验,没有合法凭证的请求直接回401,而Flink官方connector默认是不会自动帮你带上用户名密码的——它甚至不知道你集群开了认证。
这就解释了为什么网上大量示例代码连起来毫无反应:那些示例基本都建立在“ES集群没开安全功能”的假设上。一旦服务端开了认证,你用new HttpHost("localhost", 9200, "http")这种方式建好连接,再往Requests.indexRequest("flink_test")里塞数据,请求发到ES那边,第一件事就是被安全拦截器拦住,返回401 Unauthorized。
再叠加一个更隐蔽的点:ES 7.x开启安全认证后,很多人会顺手把TLS也打开。这时候Flink端的rest client不仅要带上凭证,还得信任ES服务端的SSL证书。否则请求根本到不了“校验密码”这一步——握手阶段就断了,报SSLHandshakeException或者PKIX path building failed。这俩坑经常同时出现,新手往往盯着401在哪调,调完发现握手又挂了,非常劝退。
在实际项目里,我还见过一种情况:集群确实开了认证,但开发环境没开,开发机上调通了,一上测试环境立刻报401。排查了一圈发现是两套集群的配置不一致。所以你在动手写代码之前,第一步永远是确认目标ES集群到底开了什么、没开什么。这一步省不得,省了后面全是猜。
2. 动手前先确认:ES安全配置、连接器版本与Flink接入方式
2.1 先搞清楚ES侧到底开启了什么
这个动作很简单,一条命令的事:
curl -u flink_user:password http://your-es-host:9200/如果返回类似{"error":{"root_cause":[{"type":"security_exception"...或者直接401,说明认证确实开了。如果这条命令都通过不了,别急着调Flink,先把ES侧的用户名密码、用户权限、HTTPS证书这三样确认清楚。
ES 7.x里,给Flink用的账号不建议直接用内置的elastic超级用户。正确做法是给Flink建一个专用账号,最小权限只给目标索引的读写权限。比如用Kibana或者API创建一个角色,只赋予write、create_index等索引级权限,然后把这个角色绑到你的Flink用户上。这样即使凭证泄露,影响面也有限。
还要确认一下ES服务端的TLS状态。在ES配置里看xpack.security.http.ssl.enabled是否为true。如果是,你后续不仅要配凭证,还要配证书信任链;如果只是认证没开TLS,那事情就简单一半。很多人一上来就报SSL握手错误,就是因为没看清楚这一项。
2.2 连接器版本别乱配,版本是最大的盘外招
Flink的ES连接器是个独立artifact,版本号一般跟Flink大版本对齐。你用的是Flink 1.15,就找flink-connector-elasticsearch7:1.15.*;Flink 1.17就找对应1.17的版本。不要图省事,随便拿一个高版本连接器塞进老Flink里跑,ClassNotFound、NoSuchMethodError会教做人。
另外一个更隐蔽的坑:连接器内部会打包一个特定版本的Elasticsearch rest client库。如果你的项目里还手动引入了其他版本的ES客户端,或者引入了elasticsearch-rest-high-level-client,很容易出现依赖冲突。典型症状是能编译能打包,一运行就NoSuchMethodError,或者各种奇怪的java.lang.ClassCastException。建议提交任务前用dependency:tree检查一遍依赖树,把项目里多余的ES客户端依赖排除掉。
如果你用的是Flink SQL,注意:官方并没有提供一个开箱即用的ES SQL Connector。网上那些“Flink SQL直接写ES”的教程,大多要么是自定义了TableFactory,要么是借助第三方中间件,跟你想象中的“官方支持”不是一回事。所以下面的代码部分,我以DataStream API的官方connector为主线来讲,也是生产里最稳的一条路。
2.3 你是用DataStream还是SQL,决定后面怎么走
简单给个方向建议:如果ES只是做结果落地,DataStream API +官方sink是首选;如果团队大量用SQL,也不得不接ES,那通常的做法是自己包一层DataStream sink,再包装成UDF或者TableFunction,别指望有现成的SQL连接器能直接填配置。
搞清楚了自己走的哪条路,下面的认证代码才有落点。我见过太多人拿着SQL的文档,然后在Java代码里反复尝试Configuration.setString,完全对不上号,纯粹浪费时间。
3. 带密码认证的两种标准姿势:内置配置与自定义RestClientFactory
ES 7.x的rest client在发起HTTP请求时,默认不会带任何认证头。要让Flink的ES连接器带上用户名密码,核心就是往它底层的HTTP client里塞一个CredentialsProvider。实现方式有两种,一种靠连接器内置配置项,另一种是自定义RestClientFactory全权接管。
3.1 姿势一:直接用连接器内置认证配置,代码最省事
Flink连接器的RestClientFactory接口里有一组约定好的配置key,其中就包括es.net.http.auth.user和es.net.http.auth.password。你可以在StreamExecutionEnvironment里拿到全局配置对象,然后把这两个值塞进去,最后构建sink时传进去:
import org.apache.flink.api.common.functions.RuntimeContext; import org.apache.flink.api.java.utils.ParameterTool; import org.apache.flink.configuration.Configuration; import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; import org.apache.flink.streaming.connectors.elasticsearch7.ElasticsearchSink; import org.apache.http.HttpHost; import org.elasticsearch.action.index.IndexRequest; import org.elasticsearch.client.Requests; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; public class EsAuthSinkJob { public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); Configuration config = new Configuration(); config.setString("es.net.http.auth.user", "flink_user"); config.setString("es.net.http.auth.password", "your_password"); List<HttpHost> httpHosts = java.util.Collections.singletonList( new HttpHost("your-es-host", 9200, "http") ); ElasticsearchSink.Builder<String> sinkBuilder = new ElasticsearchSink.Builder<>( httpHosts, new SimpleStringElasticsearchSinkFunction() ); sinkBuilder.setBulkFlushMaxActions(1000); sinkBuilder.setBulkFlushIntervalMillis(5000); env.fromElements("hello", "world") .addSink(sinkBuilder.build()) .name("es-auth-sink"); env.execute("es-auth-sink-job"); } public static class SimpleStringElasticsearchSinkFunction implements org.apache.flink.streaming.connectors.elasticsearch.ElasticsearchSinkFunction<String> { @Override public void process(String element, RuntimeContext ctx, org.apache.flink.streaming.connectors.elasticsearch.RequestIndexer indexer) { Map<String, Object> data = new HashMap<>(); data.put("value", element); data.put("timestamp", System.currentTimeMillis()); IndexRequest request = Requests.indexRequest("flink_test") .id(UUID.randomUUID().toString()) .source(data); indexer.add(request); } } }关键点在于:连接器在构建内部rest client时,会从一个Configuration对象里读出这两个key,注册成BasicCredentialsProvider,再注入到Apache HttpClient的默认凭证体系里。所以只要你设置了这两个key,后面ES请求就能带上Authorization: Basic xxx。
这里提醒一句:如果你自定义了RestClientFactory,并且configureRestClient里自己创建了CredentialsProvider,那你就是全权接管了,内置那两个key不会自动生效。两者只能选一个,别写了es.net.http.auth.user又自定义工厂,然后发现认证失效。
3.2 姿势二:自定义RestClientFactory,把控制权握在自己手里
官方内置配置的好处是省事,但如果你需要更精细的控制,比如动态从外部系统拉取密码、配置超时时间、同时处理TLS证书,那就必须自定义RestClientFactory。这个接口就一个方法configureRestClient(RestClientBuilder builder, Configuration configuration),你在这里面拿到底层RestClientBuilder做修改。
下面是一个完整且能用的认证工厂:
import org.apache.flink.configuration.Configuration; import org.apache.flink.streaming.connectors.elasticsearch.RestClientFactory; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.impl.client.BasicCredentialsProvider; import org.elasticsearch.client.RestClientBuilder; public class AuthRestClientFactory implements RestClientFactory { private final String username; private final String password; public AuthRestClientFactory(String username, String password) { this.username = username; this.password = password; } @Override public void configureRestClient(RestClientBuilder builder, Configuration configuration) { builder.setHttpClientConfigCallback(httpClientBuilder -> { CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials( new AuthScope(username, password), new UsernamePasswordCredentials(username, password) ); httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); return httpClientBuilder; }); } }注意我这里的写法用了new AuthScope(username, password),其实更常用的写法是AuthScope.ANY,表示对所有主机、端口、realm都生效。如果你知道ES侧配置的具体realm,也可以指定更精确的scope。但在生产里,AuthScope.ANY通用性更好,避免因为realm不匹配导致请求头上没带凭证。
使用这个工厂时:
ElasticsearchSink.Builder<String> sinkBuilder = new ElasticsearchSink.Builder<>( httpHosts, new SimpleStringElasticsearchSinkFunction() ); sinkBuilder.setRestClientFactory(new AuthRestClientFactory("flink_user", "your_password"));就这么简单。之后sinkBuilder.build()出来的sink,发出的每条HTTP请求都会自动携带Basic认证头。原理上其实和姿势一是相通的,只不过姿势一帮你封装好了,姿势二是让你自己写。
3.3 完整示例:从数据源到ES sink的整段可跑代码
把上面两部分拼起来,给一个完整可跑的参考。假设数据从Kafka读,写进ES,并且ES开了用户名密码认证:
import org.apache.flink.api.common.serialization.SimpleStringSchema; import org.apache.flink.streaming.api.datastream.DataStream; import org.apache.flink.streaming.api.environment.StreamExecutionEnvironment; import org.apache.flink.streaming.connectors.kafka.FlinkKafkaConsumer; import org.apache.flink.streaming.connectors.elasticsearch7.ElasticsearchSink; import org.apache.http.HttpHost; import java.util.List; import java.util.Properties; public class KafkaToEsAuthJob { public static void main(String[] args) throws Exception { StreamExecutionEnvironment env = StreamExecutionEnvironment.getExecutionEnvironment(); env.enableCheckpointing(5000); Properties kafkaProps = new Properties(); kafkaProps.setProperty("bootstrap.servers", "kafka-host:9092"); kafkaProps.setProperty("group.id", "flink-es-connector-demo"); FlinkKafkaConsumer<String> consumer = new FlinkKafkaConsumer<>( "input-topic", new SimpleStringSchema(), kafkaProps ); consumer.setStartFromLatest(); DataStream<String> stream = env.addSource(consumer); List<HttpHost> esHosts = java.util.Collections.singletonList( new HttpHost("es-host", 9200, "http") ); ElasticsearchSink.Builder<String> esSinkBuilder = new ElasticsearchSink.Builder<>( esHosts, new SimpleStringElasticsearchSinkFunction() ); esSinkBuilder.setRestClientFactory( new AuthRestClientFactory("flink_user", "your_password") ); esSinkBuilder.setBulkFlushMaxActions(1000); esSinkBuilder.setBulkFlushIntervalMillis(3000); esSinkBuilder.setBulkFlushBackoff(true); esSinkBuilder.setBulkFlushBackoffRetries(3); esSinkBuilder.setBulkFlushBackoffDelayMillis(1000); stream.addSink(esSinkBuilder.build()).name("es-auth-sink"); env.execute("kafka-to-es-auth"); } }这段代码里,setBulkFlushMaxActions和setBulkFlushIntervalMillis是控制批量刷写节奏的。启动后,Flink会攒够1000条或者每隔3秒批量写一次ES。如果ES集群写入压力大,可以适当调低setBulkFlushMaxActions,比如500甚至200,避免单批次过大被ES拒绝。
3.4 配置key查不到怎么办
有些版本里,es.net.http.auth.user这组key可能不在你手头的连接器版本里。最简单粗暴的确认办法是:去Maven仓库把你用的flink-connector-elasticsearch7的jar包下下来,用解压工具打开,然后直接搜es.net.http.auth.user这个字符串。搜得到,说明可以走姿势一;搜不到,就用姿势二自定义。
我当时排查一个诡异问题时,就是靠这个搜jar包的方法确认了手头版本到底支持哪些配置项。这个方法比翻文档快,也比网上问人准,因为网上教程针对的版本可能和你的根本不一样。
4. 如果ES还开了TLS,这个SSL坑躲不掉
4.1 报错长啥样,为什么握手过不去
ES开TLS之后,Flink的rest client默认不会信任ES服务器的自签证书。报错通常长这样:
Caused by: javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target你一看以为是不是密码错了,其实不是。这是JDK的信任库(truststore)里没有ES服务器的CA证书。Flink的rest client底层用的是Apache HttpClient,它默认加载JVM全局truststore,里面当然没有你们内网自签的ES证书。解决方法就是把ES的CA证书导入到一个truststore文件里,然后在Flink端指定这个truststore的路径和密码。
4.2 证书从ES侧导出,导入到Flink的truststore
ES侧如果用elasticsearch-certutil生成的证书,通常会有一个CA证书文件,比如elastic-stack-ca.p12。你可以从中导出CA证书:
openssl pkcs12 -in elastic-stack-ca.p12 -clcerts -nokeys -out ca.crt然后导入到Java的truststore:
keytool -importcert -file ca.crt -keystore flink_truststore.jks -storepass changeit -alias es-ca -noprompt这里要区分两个概念:truststore存的是“你信任的CA证书”,keystore存的是“你自己的私钥和证书”。Flink连接ES,需要的是信任服务端的证书,所以用的是truststore,不是keystore。很多人把服务端证书导成keystore塞给Flink,报错依旧,就是因为方向搞反了。
4.3 在RestClientFactory里同时配好SSL和认证
如果你是自定义工厂,理论上SSL和认证可以在同一个setHttpClientConfigCallback里一起配。下面这个工厂同时处理了用户名密码和truststore:
import org.apache.flink.configuration.Configuration; import org.apache.flink.streaming.connectors.elasticsearch.RestClientFactory; import org.apache.http.auth.AuthScope; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.CredentialsProvider; import org.apache.http.impl.client.BasicCredentialsProvider; import org.apache.http.ssl.SSLContextBuilder; import org.elasticsearch.client.RestClientBuilder; import javax.net.ssl.SSLContext; import java.io.File; public class AuthWithSslRestClientFactory implements RestClientFactory { private final String username; private final String password; private final String trustStorePath; private final String trustStorePassword; public AuthWithSslRestClientFactory(String username, String password, String trustStorePath, String trustStorePassword) { this.username = username; this.password = password; this.trustStorePath = trustStorePath; this.trustStorePassword = trustStorePassword; } @Override public void configureRestClient(RestClientBuilder builder, Configuration configuration) { builder.setHttpClientConfigCallback(httpClientBuilder -> { try { SSLContext sslContext = SSLContextBuilder.create() .loadTrustMaterial( new File(trustStorePath), trustStorePassword.toCharArray() ) .build(); httpClientBuilder.setSSLContext(sslContext); } catch (Exception e) { throw new RuntimeException("Failed to load truststore", e); } CredentialsProvider credentialsProvider = new BasicCredentialsProvider(); credentialsProvider.setCredentials( AuthScope.ANY, new UsernamePasswordCredentials(username, password) ); httpClientBuilder.setDefaultCredentialsProvider(credentialsProvider); return httpClientBuilder; }); } }如果你用的是内置配置方式,则仍然有对应的配置key,比如es.net.ssl.trust-store-location和es.net.ssl.trust-store-password。具体key名同样以你手头连接的版本为准,搜jar包确认一下最稳妥。
5. 从401到批量失败,我的完整排查链路
5.1 401 Unauthorized:先确认用户、密码、权限三个层次
遇到401,第一反应不是改代码,而是确认这个账号到底有没有权限访问目标索引。步骤是这样:
# 1. 先直接测ES侧 curl -u flink_user:password http://es-host:9200/flink_test/_count # 2. 如果返回401,说明账号密码不对或账号被禁用 # 3. 如果返回403,说明账号有认证但权限不足 # 4. 如果返回200,说明ES侧没问题,回去查Flink配置认证通过和权限通过是两回事。ES的安全模型是:401表示“你是谁都没验证出来”,403表示“你是你,但你不配”。很多时候你会发现curl能通过,但Flink任务一直401——那基本就是代码里没把凭证正确注入到rest client,或者写了URL传参这种无效方式。别浪费时间在ES侧,回头检查代码。
5.2 SSLHandshakeException:不要只看堆栈,去确认服务端TLS
有次我排查一个任务,日志里一堆SSL握手错误,堆栈里还有证书信息。我盯着代码改了一下午,后来一查ES配置才发现,人家的集群根本不只是认证,还强制要求HTTPS访问。这不是代码问题,是环境配对问题。
所以看到SSL相关报错,先去看ES的xpack.security.http.ssl.enabled是不是true。是的话,按照第4节的方式配置truststore;不是的话,说明你连错端口了或者连的是别的服务。还有个小细节:连接地址的协议要写对,TLS开启后ES通常暴露在https协议下,如果代码里仍然是new HttpHost(host, 9200, "http"),握手阶段同样会挂。
5.3 bulk拒绝与连接断开:问题往往不在Flink
认证搞定了,证书也导了,但跑着跑着任务报Connection closed by target host或者ES日志里出现大量rejected_execution_exception。这时候别盯着Flink日志看了,去看ES的bulk rejection指标。
最常见的导火索是批量参数设置过大。默认1000条一批,如果单条记录很大(比如里面有几个MB的嵌套JSON),一个批次可能就有几十MB,ES的HTTP请求体缓冲区一旦放不下,直接拒连。我的处理习惯是:单条数据几十KB以上时,setBulkFlushMaxActions降到100~200,再配合setBulkFlushIntervalMillis(1000),宁可多刷几次也不要一次性压垮ES。
另一个可能原因是ES集群的写入线程池被打满,GC频繁。这种情况Flink再怎么调都治标不治本,得从ES侧扩容或者加索引分片。总之,批量写入失败的定位顺序永远是:先ES侧指标,再Flink侧参数。
5.4 ClassNotFound/版本冲突:提交环境最隐蔽的定时炸弹
一个让我印象很深的场景:代码在IDE里跑得欢天喜地,一打成jar提交到集群,立刻ClassNotFoundException: org.elasticsearch.client.RestClientBuilder。原因就是集群环境自带的ES client和你提交的jar里打包的版本不一致,类加载器先加载了旧版本,方法签名对不上。
排查思路分两步:第一,用dependency:tree把项目里的ES相关依赖列出来,确认有没有相同类的不同版本;第二,确认你提交的jar是 fat jar,并且把依赖冲突排除干净。这一步在认证场景里尤其重要,因为认证逻辑强依赖Apache HttpClient和ES client的具体实现类,版本错一个字母都有可能跑挂。
5.5 想用Flink SQL连ES?先死心再想办法
还是那句话:官方没有为Flink SQL提供ES连接器。你可能会在网上看到某个第三方SQL connector,帮你写了一个ES的TableFactory,但你很难确认它是否支持认证、是否兼容你用的Flink版本。
我的经验是:如果团队非要用SQL写ES,最稳妥的方案是不折腾SQL插件,写一个DataStream sink封装成Flink UDF或TableFunction,让SQL调用Java代码完成ES写入。这样认证逻辑完全可控,不依赖第三方对你ES版本的支持。
6. 上生产之前,这些细节值得花十分钟设置
6.1 密码不能躺在代码里
把用户名密码硬编码在AuthRestClientFactory的构造函数里,demo可以,生产不行。因为你的代码会进Git仓库,密码等于公开了。
建议改成从Flink的全局配置或者环境变量读取:
flink run -D es.auth.user=flink_user -D es.auth.password=xxx -c KafkaToEsAuthJob your-fat-jar.jar然后在代码里用ParameterTool.fromArgs(args)或者GlobalJobParameters读取。如果公司有配置中心,也可以在main()里启动时拉取一次,再塞进sink builder。总之,密码的加载路径一定要和代码逻辑解耦。
6.2 最小权限用户与服务账号
再强调一次,ES侧不要用elastic超级用户跑Flink任务。这个账号权限太大,业务日志里也不该出现它的认证记录。给Flink建专用账号,角色只给目标索引前缀的写入权限,比如flink-*。这样即使某天密码泄露,攻击面也只是那几个业务索引,而不是整个集群。
6.3 多个sink连不同集群,注意工厂实例隔离
一个Flink作业里如果有两个sink,一个写开发ES、一个写生产ES,各自用了不同的凭证,那每个sink都要单独new AuthRestClientFactory(...),别图省事抽出一个全局工厂共享。因为工厂实例一旦被多个sink复用,你很难保证CredentialsProvider不串。同样,Configuration也不要塞一个任务级的全局配置给多个sink,各传各的,互不干扰。
6.4 写进监控指标的几件事
- Flink端:
numRecordsOutErrors一定要配告警,认证过期、ES拒写这类问题通常会先反映在它上面。 - ES端:重点看
bulk.rejected(批量拒绝)和thread_pool.write.rejected(写入线程池拒绝)。只要这俩指标不为0,Flink调参优化就要往“减小批量、增大重试”方向走。 - 如果ES证书过期,Flink任务会在运行中突然断连,这个没有事前告警特别难受。建议定期用curl检查证书有效期,不要等报错才发现。
6.5 你甚至可以写个探针任务
我上生产前习惯性写一个极简的“hello es”任务,只从本地集合发一条数据进ES,验证认证、SSL、权限全链路通不通。这个任务平时可以不管,出问题的时候先把业务整体停掉,跑一下探针任务,几分钟就能定位是Flink侧问题还是ES侧问题,比对着几百MB日志翻找快得多。
回到开头说的那个场景:ES 7开了密码认证之后,Flink连接失败太正常了。但说白了,你只要绕开三个坑就稳了:凭证注入要落到CredentialsProvider而不是URL参数;TLS证书要进truststore而不是keystore;连接器版本要和Flink严格对齐。把这三点刻在脑子里,以后再遇到这问题,基本就是十分钟的事。