zookeeper可以作为微服务注册中心,spring cloud也提供了zookeeper注册中心的支持。
本文介绍如何实现一个简单的zookeeper注册中心,主要的实现方式:
n个服务提供者对外提供http接口获取数据,这些服务提供者把自己的主机、端口信息注册到zookeeper的某个节点上面;
当服务提供者宕机或者服务不可用时,zookeeper节点会删除该提供者的信息;
消费者也连接zookeeper获取可以使用的服务提供者(并且会持续监听节点,节点变化时也会实时更新本地的服务提供者列表),然后发起http请求调用服务接口获取数据
下面简单介绍一下实现方式
1、UserService服务
后台用户管理服务web工程,使用spring mvc注解方式开发,不提供用户操作界面。提供http接口根据用户名获取用户信息,入参用户名字符串,响应为json类型,注册的服务名为UserService
2、UserAdminService服务
用户管理系统web工程,使用spring mvc注解方式开发,这是一个给用户使用的系统。提供登录页面、和首页,登录操作调用UserService提供的http接口根据用户输入的用户名获取用户信息,然后比对密码是否正确,服务名为UserAdminService
3、修改UserService服务工程
1)application.properties配置
1 ## zookeeper集群地址
2 # zk.url=192.168.0.28:2181,192.168.0.29:2181,192.168.0.30:2181
3 zk.url=127.0.0.1:2181
4
5 ## 注册服务使用的二级路径名
6 service.name=UserService
7 ## IP根据实际部署的服务器修改
8 server.hostname=127.0.0.1
9 ## 端口根据实际监听端口修改
10 server.port=8080
11
12 ## 注册服务时会创建
13 ## /services/${service.name}/${server.hostname}:${server.port}
14 ## 这样一个临时节点,值为${service.name}
三个服务部署在一台服务器上,分别监听7070、8080、9090,可以使用server.port参数配置
2)ZookeeperRegister类
编写ZookeeperRegister类,根据zk集群地址、服务名、IP、端口等配置注册服务到zookeeper集群
a) 读取application.properties文件,加载配置
b) zkRegist方法会在依赖注入完成后由spring容器调用
c) 获取zk集群地址、服务名、IP、端口等配置
d) 创建ZooKeeper对象,来注册服务,也就是在zookeeper创建一个临时节点
e) 节点规则是: /services/${service.name}/${server.hostname}:${server.port},值为: ${service.name}
f) 此类对象会被spring容器管理
3)ZookeeperRegister类代码
1 @Component
2 @PropertySource("classpath:application.properties")
3 public class ZookeeperRegister {
4
5 private ZooKeeper zkClient;
6
7 @Autowired
8 private Environment env;
9
10 @PostConstruct
11 public void zkRegist() throws IOException, KeeperException,
12 InterruptedException {
13
14 // 获取配置信息
15 String zkUrl = env.getProperty("zk.url");
16 String serviceName = env.getProperty("service.name");
17 String hostName = env.getProperty("server.hostname");
18 int port = Integer.parseInt(env.getProperty("server.port"));
19
20 // 创建ZooKeeper对象
21 // 超时时间为2分
22 zkClient = new ZooKeeper(zkUrl, 120000, new Watcher() {
23 @Override
24 public void process(WatchedEvent event) {
25
26 }
27 });
28
29 // path = /services/${service.name}/${server.hostname}:${server.port}
30 // value = ${service.name}
31
32 Stat exists = zkClient.exists("/services", false);
33 if (exists == null) {
34 zkClient.create("/services", "services".getBytes(),
35 Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT);
36 exists = zkClient.exists("/services/UserService", false);
37 if (exists == null) {
38 zkClient.create("/services/" + serviceName,
39 serviceName.getBytes(), Ids.OPEN_ACL_UNSAFE,
40 CreateMode.PERSISTENT);
41 }
42 }
43 // 创建znode节点
44 // 临时节点
45 zkClient.create("/services/" + serviceName + "/" + hostName + ":"
46 + port, serviceName.getBytes(), Ids.OPEN_ACL_UNSAFE,
47 CreateMode.EPHEMERAL);
48 }
49 }
View Code
4、修改UserAdminService服务工程
1)application.properties配置
1 ## zookeeper集群地址
2 # zk.url=192.168.0.28:2181,192.168.0.29:2181,192.168.0.30:2181
3 zk.url=127.0.0.1:2181
2)UserServiceImpl类
UserServiceImpl类实现Watcher和UserService接口,使用@Service标注,由spring容器管理
a) 初始化方法中创建ZooKeeper对象,连接到zk集群
b) 当ZooKeeper中服务提供者节点发生变化时调用getProviders方法重新获取可用的服务提供者
c) getUserByUsername方法先从当前可用的提供者集合中随机获取一个提供者,使用httpclient发送请求获取用户数据返回给调用者
把UserService注入到Controller即可使用
3)UserServiceImpl类代码
1 @Service
2 @PropertySource("classpath:application.properties")
3 public class UserServiceImpl implements UserService, Watcher {
4
5 private static Logger log = LoggerFactory.getLogger(UserServiceImpl.class);
6
7 private static final String SERVICE_URL =
8 "/UserService/user/getUserByUsername?username=";
9 private static final String SERVICE_REGIST_PATH = "/services/UserService";
10
11 @Autowired
12 private Environment env;
13
14 private ZooKeeper zkClient;
15 private List<String> providers = new ArrayList<String>();
16
17 @PostConstruct
18 public void init() {
19 String zkUrl = env.getProperty("zk.url");
20 try {
21 zkClient = new ZooKeeper(zkUrl, 120000, this);
22 } catch (IOException e) {
23 throw new RuntimeException(e.getMessage(), e);
24 }
25 }
26
27 @Override
28 public User getUserByUsername(String username) {
29
30 User user = null;
31
32 // 获取一个可以使用的服务提供者
33 String provider = getProvider();
34
35 String url = "http://" + provider + SERVICE_URL + username;
36
37 // 发送请求获取数据
38 CloseableHttpClient http = HttpClients.createDefault();
39
40 HttpGet httpGet = null;
41 CloseableHttpResponse response = null;
42 InputStream in = null;
43
44 try {
45 // 构建httpget
46 httpGet = new HttpGet(new URI(url));
47
48 // 发送请求获取响应
49 response = http.execute(httpGet);
50 HttpEntity entity = response.getEntity();
51
52 // 获取输入流和数据字符串
53 in = entity.getContent();
54 String json = IOUtils.toString(in, "utf-8");
55
56 log.info(String.format("请求: %s, 响应: %s", url, json));
57
58 // 解析json字符串为user对象
59 user = json2User(json);
60
61 } catch (URISyntaxException e) {
62 e.printStackTrace();
63 } catch (ClientProtocolException e) {
64 e.printStackTrace();
65 } catch (IOException e) {
66 e.printStackTrace();
67 } finally {
68 // 关闭输入流和响应对象
69 IOUtils.closeQuietly(in);
70 IOUtils.closeQuietly(response);
71 }
72 return user;
73 }
74
75 @Override
76 public void process(WatchedEvent event) {
77 getProviders();
78 }
79
80 private void getProviders() {
81 try {
82 this.providers = zkClient.getChildren(SERVICE_REGIST_PATH, true);
83 log.info(String.format("可用服务提供者: %s", this.providers));
84 } catch (KeeperException e) {
85 e.printStackTrace();
86 } catch (InterruptedException e) {
87 e.printStackTrace();
88 }
89 }
90
91 private String getProvider() {
92 int size = this.providers.size();
93 Random r = new Random();
94 int i = r.nextInt(size);
95 String provider = this.providers.get(i);
96 log.info(String.format("可用服务提供者: %s, 随机获取提供者 [%s], 序号 [%s]",
97 this.providers, provider, i));
98 return provider;
99 }
100
101 private User json2User(String json) {
102 ObjectMapper mapper = new ObjectMapper();
103 User user = null;
104 try {
105 user = mapper.readValue(json, User.class);
106 } catch (JsonParseException e) {
107 e.printStackTrace();
108 } catch (JsonMappingException e) {
109 e.printStackTrace();
110 } catch (IOException e) {
111 e.printStackTrace();
112 }
113 return user;
114 }
115 }
View Code
5、部署测试
1)部署启动zookeeper
2)服务部署
服务提供者和消费者是部署在同一台机器上面的,监听端口不同
部署三个UserService服务,修改server.port参数,分别监听7070、8080、9090
部署UserAdminService服务,我测试的时候和其中一个服务提供者部署在了同一个tomcat里面,监听9090
其中7070和8080的tomcat使用cmd命令行启动,9090的tomcat在eclipse里面启动便于观察服务消费者的日志
启动了三个tomcat之后可以看看zookeeper的变化
另外,在UserAdminService服务的日志中可以看到当前可以使用的服务提供者列表
访问一下http://localhost:9090/UserAdminWeb/login登录,可以看到后台程序获取到了一个可以使用的服务提供者,然后发起了http请求查询用户信息
当某个服务提供者挂掉之后,可以看到消费者的日志,已经重新获取了提供者列表
6、存在的一些问题
a) 服务提供者突然挂掉之后,zookeeper并不能马上删除该提供者的znode信息,所以在服务消费者这边还需要做一些优化,即发起请求获取用户信息时如果出现问题,应该从可用服务提供者列表删除该提供者信息
b) 服务消费者使用的httpclient发起请求,如果每次获取用户信息都去重新建立tcp连接,效率很低,所以需要使用连接池技术管理用于发起http请求的tcp连接