在大型分布式系統(tǒng)中特石,有很多的微服務(wù)對(duì)外提供服務(wù),也會(huì)有各種微服務(wù)的協(xié)議需要集成鳖链,比如http姆蘸,https,grpc的芙委,這時(shí)就需要一個(gè)API網(wǎng)關(guān)提供高性能逞敷、高可用的API托管服務(wù),幫助服務(wù)的開發(fā)者便捷地對(duì)外提供服務(wù)灌侣,而不用考慮安全控制推捐、流量控制、審計(jì)日志等問題侧啼,統(tǒng)一在網(wǎng)關(guān)層將安全認(rèn)證牛柒,流量控制堪簿,審計(jì)日志,黑白名單等實(shí)現(xiàn)焰络。網(wǎng)關(guān)的下一層戴甩,是內(nèi)部服務(wù),內(nèi)部服務(wù)只需開發(fā)和關(guān)注具體業(yè)務(wù)相關(guān)的實(shí)現(xiàn)闪彼。網(wǎng)關(guān)可以提供API發(fā)布甜孤、管理、維護(hù)等主要功能畏腕。開發(fā)者只需要簡(jiǎn)單的配置操作即可把自己開發(fā)的服務(wù)發(fā)布出去缴川,同時(shí)置于網(wǎng)關(guān)的保護(hù)之下。我們項(xiàng)目中使用的API Gateway是Kong描馅,在代理grpc服務(wù)的時(shí)候把夸,我們需要將此grpc服務(wù)以http/https協(xié)議提供給前端訪問,因此需要用到Kong提供的grpc-web插件來(lái)幫忙將http/https的請(qǐng)求代理到后端的grpc服務(wù)上铭污,在kong-plugin-grpc-web的官網(wǎng)上提供了配置實(shí)例恋日,但按照此實(shí)例配置沒法代理成功,而且基本網(wǎng)上都沒有找到其他demo和資料嘹狞,解決過(guò)程很簡(jiǎn)單岂膳,就是根據(jù)錯(cuò)誤信息查看源碼,分析出實(shí)際項(xiàng)目中該如何使用grpc-web插件來(lái)配置grpc服務(wù)磅网。
關(guān)于Kong
Kong是一款基于Nginx_Lua模塊寫的高可用谈截,易擴(kuò)展由Mashape公司開源的API Gateway項(xiàng)目。由于Kong是基于Nginx的涧偷,所以可以水平擴(kuò)展多個(gè)Kong服務(wù)器簸喂,通過(guò)前置的負(fù)載均衡配置把請(qǐng)求均勻地分發(fā)到各個(gè)Server,來(lái)應(yīng)對(duì)大批量的網(wǎng)絡(luò)請(qǐng)求燎潮。Kong采用插件機(jī)制進(jìn)行功能定制喻鳄,插件集(可以是0或n個(gè))在API請(qǐng)求響應(yīng)循環(huán)的生命周期中被執(zhí)行。插件使用Lua編寫跟啤,目前已有幾個(gè)基礎(chǔ)功能:HTTP基本認(rèn)證诽表、密鑰認(rèn)證、CORS( Cross-origin Resource Sharing隅肥,跨域資源共享)、TCP袄简、UDP腥放、文件日志、API請(qǐng)求限流绿语、請(qǐng)求轉(zhuǎn)發(fā)以及nginx監(jiān)控秃症。這篇文章中我們會(huì)用到另一個(gè)開源的插件候址,官網(wǎng)地址:
https://github.com/Kong/kong-plugin-grpc-web
關(guān)于Kong gRPC-Web插件
A Kong plugin to allow access to a gRPC service via the gRPC-Web protocol. Primarily, this means JS browser apps using the gRPC-Web library.
A service that presents a gRPC API can be used by clients written in many languages, but the network specifications are oriented primarily to connections within a datacenter. In order to expose the API to the Internet, and to be called from brower-based JS apps, gRPC-Web was developed.This plugin translates requests and responses between gRPC-Web and "real" gRPC. Supports both HTTP/1.1 and HTTP/2, over plaintext (HTTP) and TLS (HTTPS) connections.
這是來(lái)自官網(wǎng)的描述,簡(jiǎn)單的說(shuō)就是開發(fā)者可以使用任何語(yǔ)言開發(fā)gRPC服務(wù)种柑,前端js程序需要通過(guò)gRPC-Web協(xié)議來(lái)訪問gRPC服務(wù)岗仑,使用此插件可以實(shí)現(xiàn)使用HTTP REST請(qǐng)求來(lái)請(qǐng)求后端的gRPC服務(wù),請(qǐng)求和返回?cái)?shù)據(jù)是json格式聚请。
Springboot開發(fā)grpc服務(wù)
為了測(cè)試gRPC代理荠雕,使用springboot開發(fā)一個(gè)簡(jiǎn)單的gRPC服務(wù)。
第一步驶赏,定義proto文件炸卑,并放在src/main/proto目錄下,命名為HelloWorld.proto
syntax?=?"proto3";
option?java_multiple_files?=?true;
package?com.example.grpc.helloworld;
message?Person?{
??string?first_name?=?1;
??string?last_name?=?2;
}
message?Greeting?{
??string?message?=?1;
}
service?HelloWorldService?{
??rpc?sayHello?(Person)?returns?(Greeting);}
第二步煤傍,設(shè)置Maven
注意以下幾個(gè)地方盖文,spring-boot-starter-web,protobuf-maven-plugin和build-helper-maven-plugin蚯姆。spring-boot-starter-web將自動(dòng)的使用內(nèi)置的tomcat來(lái)部署grpc服務(wù)五续。protobuf-maven-plugin是用來(lái)根據(jù)定義的proto文件來(lái)生成grpc-based的java代碼。build-helper-maven-plugin此插件是用來(lái)設(shè)置將產(chǎn)生的java source code作為編譯的一部分龄恋,也能幫助IntellJ工具找到源碼疙驾,否則雖然編譯能成功,但IntellJ會(huì)出現(xiàn)很多的紅線提示錯(cuò)誤篙挽,不太友好荆萤,而且也不能F3定位到源碼的位置。
<?xml?version="1.0"?encoding="UTF-8"?>
<project?xmlns="http://maven.apache.org/POM/4.0.0"?xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
????xsi:schemaLocation="http://maven.apache.org/POM/4.0.0?https://maven.apache.org/xsd/maven-4.0.0.xsd">
????<modelVersion>4.0.0</modelVersion>
????<parent>
????????<groupId>org.springframework.boot</groupId>
????????<artifactId>spring-boot-starter-parent</artifactId>
????????<version>2.3.1.RELEASE</version>
????????<relativePath/>?<!--?lookup?parent?from?repository?-->
????</parent>
????<groupId>com.example.grpc</groupId>
????<artifactId>demo</artifactId>
????<version>0.0.1-SNAPSHOT</version>
????<name>demo</name>
????<description>Demo?project?for?Spring?Boot</description>
????<properties>
????????<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
????????<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
????????<java.version>1.8</java.version>
????????<grpc-spring-boot-starter.version>3.0.0</grpc-spring-boot-starter.version>
????????<os-maven-plugin.version>1.6.1</os-maven-plugin.version>
????????<protobuf-maven-plugin.version>0.6.1</protobuf-maven-plugin.version>
????</properties>
????<dependencies>
????????<dependency>
????????????<groupId>org.springframework.boot</groupId>
????????????<artifactId>spring-boot-starter-web</artifactId>
????????</dependency>
????????<dependency>
????????????<groupId>io.github.lognet</groupId>
????????????<artifactId>grpc-spring-boot-starter</artifactId>
????????????<version>${grpc-spring-boot-starter.version}</version>
????????</dependency>
????????<dependency>
????????????<groupId>org.springframework.boot</groupId>
????????????<artifactId>spring-boot-starter-test</artifactId>
????????????<scope>test</scope>
????????</dependency>
????????<dependency>
????????????<groupId>org.springframework.boot</groupId>
????????????<artifactId>spring-boot-starter-log4j2</artifactId>
????????</dependency>
????????<dependency>
????????????<groupId>org.projectlombok</groupId>
????????????<artifactId>lombok</artifactId>
????????</dependency>
????</dependencies>
????<build>
????????<extensions>
????????????<extension>
????????????????<groupId>kr.motd.maven</groupId>
????????????????<artifactId>os-maven-plugin</artifactId>
????????????????<version>${os-maven-plugin.version}</version>
????????????</extension>
????????</extensions>
????????<plugins>
????????????<plugin>
????????????????<groupId>org.springframework.boot</groupId>
????????????????<artifactId>spring-boot-maven-plugin</artifactId>
????????????</plugin>
????????????<plugin>
????????????????<groupId>org.xolstice.maven.plugins</groupId>
????????????????<artifactId>protobuf-maven-plugin</artifactId>
????????????????<version>${protobuf-maven-plugin.version}</version>
????????????????<configuration>
????????????????????<protocArtifact>com.google.protobuf:protoc:3.5.1-1:exe:${os.detected.classifier}</protocArtifact>
????????????????????<pluginId>grpc-java</pluginId>
????????????????????<pluginArtifact>io.grpc:protoc-gen-grpc-java:1.16.1:exe:${os.detected.classifier}</pluginArtifact>
????????????????</configuration>
????????????????<executions>
????????????????????<execution>
????????????????????????<goals>
????????????????????????????<goal>compile</goal>
????????????????????????????<goal>compile-custom</goal>
????????????????????????</goals>
????????????????????</execution>
????????????????</executions>
????????????</plugin>
????????????<plugin>
????????????????<groupId>org.codehaus.mojo</groupId>
????????????????<artifactId>build-helper-maven-plugin</artifactId>
????????????????<version>1.4</version>
????????????????<executions>
????????????????????<execution>
????????????????????????<id>test</id>
????????????????????????<phase>generate-sources</phase>
????????????????????????<goals>
????????????????????????????<goal>add-source</goal>
????????????????????????</goals>
????????????????????????<configuration>
????????????????????????????<sources>
????????????????????????????????<source>${basedir}/target/generated-sources</source>
????????????????????????????</sources>
????????????????????????</configuration>
????????????????????</execution>
????????????????</executions>
????????????</plugin>
????????</plugins>
????</build>
</project>
第三步铣卡,實(shí)現(xiàn)grpc服務(wù)
服務(wù)實(shí)現(xiàn)代碼很簡(jiǎn)單链韭,如下:
@GRpcService
@Slf4j
public?class?HelloWorldServiceImpl
????????extends?HelloWorldServiceGrpc.HelloWorldServiceImplBase?{
????@Override
????public?void?sayHello(Person?request,?????????????????????????StreamObserver<Greeting>?responseObserver)?{
????????log.info("server?received?{}",?request);
????????String?message?=?"Hello?"?+?request.getFirstName()?+?"?"
????????????????+?request.getLastName()?+?"!";
????????Greeting?greeting?=
????????????????Greeting.newBuilder().setMessage(message).build();
????????log.info("server?responded?{}",?greeting);
????????responseObserver.onNext(greeting);
????????responseObserver.onCompleted();
????}
}
使用springboot運(yùn)行此項(xiàng)目,將默認(rèn)啟動(dòng)6565端口發(fā)布此grpc服務(wù)煮落。下面我們就使用grpc-web 插件部署此服務(wù)
安裝KONG
第一步敞峭,創(chuàng)建Docker網(wǎng)絡(luò)
docker?network?create?kong-net
第二步,安裝Postgresql或者Cassandra
安裝Cassandra作為存儲(chǔ)
docker?run?-d?--name?kong-database?\
???????????????--network=kong-net?\
???????????????-p?9042:9042?\
???????????????cassandra:3
或者安裝Postgresql作為存儲(chǔ)
docker?run?-d?--name?kong-database?\
???????????????--network=kong-net?\
???????????????-p?5432:5432?\
???????????????-e?"POSTGRES_USER=kong"?\
???????????????-e?"POSTGRES_DB=kong"?\
???????????????-e?"POSTGRES_PASSWORD=kong"?\
???????????????postgres:9.6
第三步蝉仇,初始kong數(shù)據(jù)
docker?run?--rm?\
?????--network=kong-net?\
?????-e?"KONG_DATABASE=postgres"?\
?????-e?"KONG_PG_HOST=kong-database"?\
?????-e?"KONG_PG_USER=kong"?\
?????-e?"KONG_PG_PASSWORD=kong"?\
?????-e?"KONG_CASSANDRA_CONTACT_POINTS=kong-database"?\
?????kong:latest?kong?migrations?bootstrap
第四步旋讹,啟動(dòng)kong
docker?run?-d?--name?kong?\
?????--network=kong-net?\
?????-e?"KONG_DATABASE=postgres"?\
?????-e?"KONG_PG_HOST=kong-database"?\
?????-e?"KONG_PG_USER=kong"?\
?????-e?"KONG_PG_PASSWORD=kong"?\
?????-e?"KONG_CASSANDRA_CONTACT_POINTS=kong-database"?\
?????-e?"KONG_PROXY_ACCESS_LOG=/dev/stdout"?\
?????-e?"KONG_ADMIN_ACCESS_LOG=/dev/stdout"?\
?????-e?"KONG_PROXY_ERROR_LOG=/dev/stderr"?\
?????-e?"KONG_ADMIN_ERROR_LOG=/dev/stderr"?\
?????-e?"KONG_ADMIN_LISTEN=0.0.0.0:8001,?0.0.0.0:8444?ssl"?\
?????-p?8000:8000?\
?????-p?8443:8443?\
?????-p?127.0.0.1:8001:8001?\
?????-p?127.0.0.1:8444:8444?\
?????kong:latest
使用grpc-web插件
完成了Kong的安裝和初始化,簡(jiǎn)單的grpc服務(wù)也完成轿衔,現(xiàn)在就按照grpc-web官網(wǎng)上的方法來(lái)設(shè)置grpc-web插件沉迹。使用的是Kong admin命令來(lái)設(shè)置,最新版的kong-dashboard暫時(shí)沒有升級(jí)到支持最新版的KONG害驹,
第一步鞭呕,創(chuàng)建grpc服務(wù)
curl?-XPOST?localhost:8001/services?\
??--data?name=grpc?\
??--data?protocol=grpc?\
??--data?host=localhost?\
??--data?port=6565
第二步,創(chuàng)建http route
curl?-XPOST?localhost:8001/services/grpc/routes?\
??--data?protocols=http?\
??--data?name=web-service?\
??--data?paths=/
第三步宛官,為此route設(shè)置grpc-web
curl?-XPOST?localhost:8001/routes/web-service/plugins?\
??--data?name=grpc-web
部署完成后葫松,訪問網(wǎng)址http://localhost:8000,返回400錯(cuò)誤瓦糕,原因unkonwn path /。
問題分析
出現(xiàn)這個(gè)錯(cuò)誤腋么,說(shuō)明KONG已經(jīng)接收到了此請(qǐng)求咕娄,但后端grpc服務(wù)沒有收到請(qǐng)求,因此問題就出現(xiàn)在grpc-web插件上珊擂,于是打開grpc-web插件源碼來(lái)進(jìn)行分析圣勒。找到下面的代碼片段:
?local?dec,?err?=?deco.new(
????kong_request_get_header("Content-Type"),
????kong_request_get_path(),?conf.proto)
??if?not?dec?then
????kong.log.err(err)
????return?kong_response_exit(400,?err)
??end
此代碼段的第五行返回400錯(cuò)誤,因此問題可能發(fā)生在deco.new方法上未玻,此方法返回nil導(dǎo)致錯(cuò)誤發(fā)生灾而,于是繼續(xù)向上分析deco.new方法,代碼如下:
function?deco.new(mimetype,?path,?protofile)
??local?text_encoding?=?text_encoding_from_mime[mimetype]
??local?framing?=?framing_form_mime[mimetype]
??local?msg_encoding?=?msg_encodign_from_mime[mimetype]
??local?input_type,?output_type
??if?msg_encoding?~=?"proto"?then
????if?not?protofile?then
??????return?nil,?"transcoding?requests?require?a?.proto?file?defining?the?service"
????end
????input_type,?output_type?=?rpc_types(path,?protofile)
????if?not?input_type?then
??????return?nil,?output_type
????end
??end
??return?setmetatable({
????mimetype?=?mimetype,
????text_encoding?=?text_encoding,
????framing?=?framing,
????msg_encoding?=?msg_encoding,
????input_type?=?input_type,
????output_type?=?output_type,
??},?deco)
end
此處扳剿,有兩處返回nil旁趟,第一處返回nil,應(yīng)該不可能庇绽,因?yàn)槲覀冊(cè)O(shè)置了proto文件锡搜,所以懷疑應(yīng)該是第二處的nil導(dǎo)致的,因此繼續(xù)向上查找rpc_types方法瞧掺,代碼如下:
local?function?rpc_types(path,?protofile)
??if?not?protofile?then
????return?nil
??end
??local?info?=?get_proto_info(protofile)
??local?types?=?info[path]
??if?not?types?then
????return?nil,?("Unkown?path?%q"):format(path)
??end
??return?types[1],?types[2]
end
此代碼中出錯(cuò)信息是Unknown path耕餐,因此基本斷定是此處返回的錯(cuò)誤信息,是由于info這個(gè)表中不能那個(gè)找到我們傳入的path辟狈,繼續(xù)向上分析如何產(chǎn)生這個(gè)info表的肠缔。也就是get_proto_info方法。
local?function?get_proto_info(fname)
??local?info?=?_proto_info[fname]
??if?info?then
????return?info
??end
??local?p?=?protoc.new()
??local?parsed?=?p:parsefile(fname)
??info?=?{}
??for?_,?srvc?in?ipairs(parsed.service)?do
????for?_,?mthd?in?ipairs(srvc.method)?do
??????info[("/%s.%s/%s"):format(parsed.package,?srvc.name,?mthd.name)]?=?{
????????mthd.input_type,
????????mthd.output_type,
??????}
????end
??end
??_proto_info[fname]?=?info
??p:loadfile(fname)
??return?info
end
從這個(gè)方法就能明白是如何產(chǎn)生info表的哼转,首先從緩存中獲取明未,如果能獲取到直接返回,否則就解析proto文件生成info表壹蔓,生成規(guī)則是proto里的package name趟妥,servicename和method name作為key,格式為info[("/%s.%s/%s"):format(parsed.package, srvc.name, mthd.name)] 佣蓉,方法的input_type和output_type作為value披摄。因此我么傳入的/這個(gè)key肯定不能存在,所以會(huì)返回400錯(cuò)誤勇凭。
水落石出
問題定位到了疚膊,解決方法就比較簡(jiǎn)單,在創(chuàng)建route時(shí)虾标,path不能按照文檔的實(shí)例中傳入簡(jiǎn)單的/,而應(yīng)該傳入/package name.service name/method name這種格式酿联。于是將創(chuàng)建route那步改成如下請(qǐng)求即可。
curl?-XPOST?localhost:8001/services/grpc/routes?\
??--data?protocols=http?\
??--data?name=web-service?\
??--data?paths=/com.example.grpc.helloworld.HelloWorldService/sayHello
測(cè)試
curl??-H?"Content-type:?application/json"?-XPOST?-d?'{"first_name":?"david","last_name":?"zhang"}'??http://localhost:8000/com.example.grpc.helloworld.HelloWorldService/sayHello?
返回字符串:hello夺巩,david zhang贞让!
寫在最后
雖然花了很多時(shí)間最后解決了此問題,也能收獲一點(diǎn)點(diǎn)的成就感柳譬,但總覺得此插件官網(wǎng)的文檔太過(guò)簡(jiǎn)單喳张,不知道是不是代碼和文檔沒有配套更新導(dǎo)致的,理論上不應(yīng)該犯這種低級(jí)錯(cuò)誤美澳,而且同一篇文章被引用了很多地方销部,都是同一種配置方法,估計(jì)都沒有親自驗(yàn)證就發(fā)布了制跟。聯(lián)系到前幾天看到一篇公眾號(hào)文章舅桩,發(fā)現(xiàn)里面有些問題,所以給作者留言了雨膨,我倒不是要較真擂涛,只是覺得一篇受眾那么廣的博客,還是需要對(duì)讀者負(fù)責(zé)聊记,不能誤導(dǎo)讀者撒妈。作者倒反饋很迅速,只是不太友好排监,哪里錯(cuò)誤狰右?說(shuō)我沒有親自驗(yàn)證,呵呵舆床,當(dāng)我把錯(cuò)誤列出來(lái)后棋蚌,也沒再回復(fù)我,悄悄的把錯(cuò)誤改了挨队。