上一篇文章簡單介紹了ZooKeeper,講了分布式中,每個微服務都會部署到多台服務器上,那服務之間的調用是怎么樣的呢?如圖:
1、集群A中的服務調用者如何發現集群B中的服務提供者呢?
2、集群A中的服務調用者如何選擇集群B中的某一台服務提供者去調用呢?
3、集群B中某台機器下線,集群A怎么避免下次調用不在使用這台掉線的機器?
4、集群B提供的某個服務如何獲知集群A中哪些機器正在消費該服務?
這篇文章寫兩個微服務,將兩個服務部署到多台服務器中 ,通過將服務注冊到ZooKeeper中,實現服務之間的調用。最終實現下面的ZooKeeper節點,然后通過服務節點下的地址,進行遠程調用。
一、服務實現
一個獲取訂單的服務和顧客信息的服務,服務之間調用是通過訂單服務查詢此訂單顧客的信息。 涉及的兩個實體Order和Customer.
public class Custormer //顧客實體 { public int Id { get; set; } public string Name { get; set; } public string Phone { get; set; } }
public class Order //訂單實體 { public int Id { get; set; } public int CustomerId { get; set; } public string Goods { get; set; } public string Address { get; set; } public Custormer Custormer; }
訂單實體中包含此訂單顧客的引用。
創建一個訂單微服務項目,實現獲取訂單列表的服務:
[Route("Order/GetOrders")] public async Task<List<Order>> GetOrders() { List<Order> orders = new List<Order>(); Order order = null; HttpClient client = new HttpClient(); for (var i = 0; i < 10; i++) { order = new Order(); order.Address = "浙江省杭州市拱墅區北部軟件園" + i; order.CustomerId = i; order.Goods = "麻辣香鍋" + i; order.Id = i; //這里需要調用獲取顧客信息服務,獲取顧客信息。這里先寫null order.Custormer = null; orders.Add(order); } return orders; }
創建一個顧客微服務項目,實現獲取顧客信息的服務:
public class CustomerController : ControllerBase { [Route("Customer/GetCustomer")] public Custormer GetCustomer(int Id) { return new Custormer() { Id=Id,Name="MicroHeart"+Id,Phone="1234567"}; } }
二、服務注冊到ZooKeeper中
兩個服務寫完了,上篇講的在服務啟動的時候,需要將服務注冊到ZooKeeper中,服務調用者啟動的時候,將服務提供或者信息從注冊中心下拉倒服務調用者本機緩存。當需要調用服務時,從本地緩存列表中找到服務提供者的地址列表,基於某種負載均衡策略(隨機、輪詢等)選擇一台服務器發起遠程調用。
在兩個項目中的Startup構造函數中,調用下面方法,保證服務啟動時就在ZooKeeper中注冊服務。
public void InitZooKeeper() { var MyApp = "/MyApp"; //創建ZooKeeper 我就不在本地創建了 客戶端和服務端都在本地的話,會造成誤會 ZooKeeper zooKeeper = new ZooKeeper("118.24.96.212:2181", 50000, new MyWatcher()); //創建 MyApp節點,數據為:MyAppData 權限控制為:開放 節點類型為:持久性節點 if (zooKeeper.existsAsync(MyApp) != null) zooKeeper.createAsync(MyApp, Encoding.UTF8.GetBytes("MyAppData"), Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); //通過反射獲取所有Controller下的方法,在獲取方法上的Route特性,通過特性設置ZooKeeper節點。 Dictionary<string, List<string>> serviceAndApiPaths = new Dictionary<string, List<string>>(); var types = System.Reflection.Assembly.GetExecutingAssembly().GetTypes(); foreach (var type in types) { if (type.BaseType == typeof(ControllerBase)) { var methods = type.GetMethods(); foreach (var method in methods) { foreach (var customAttribute in method.CustomAttributes) { if (customAttribute.AttributeType == typeof(RouteAttribute)) { var serviceName = type.Name.Replace("Controller", "Services"); if (!serviceAndApiPaths.Keys.Contains(serviceName)) { List<string> apiPaths = new List<string>();
//因為Route的值帶"/" 會導致ZooKeeper認為是節點符號,所以要轉換一下 apiPaths.Add(customAttribute.ConstructorArguments[0].ToString().Replace("/","-")); serviceAndApiPaths.Add(serviceName, apiPaths); } else serviceAndApiPaths[serviceName].Add(customAttribute.ConstructorArguments[0].ToString().Replace("/", "-")); } } } } } //將這些接口列表 放到MyApp節點下 foreach(var item in serviceAndApiPaths) { //創建 服務節點,為持久性節點 if (zooKeeper.existsAsync($@"{MyApp}/{item.Key}") != null) zooKeeper.createAsync($@"{MyApp}/{item.Key}", null, Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); foreach (var apiPath in item.Value) { //創建 Api節點,為持久性節點 if (zooKeeper.existsAsync($@"{MyApp}/{item.Key}/{apiPath}") != null) zooKeeper.createAsync($@"{MyApp}/{item.Key}/{apiPath}", null, Ids.OPEN_ACL_UNSAFE, CreateMode.PERSISTENT); //創建 Ip+port 節點,為臨時性節點(由於我本地 不能通過我局域網Ip地址訪問,所以我寫死127.0.0.1) 寫成臨時節點 是因為 //當這個客戶端與服務端斷開時,對應的節點自動消失了。 //IPAddress[] IPList = System.Net.Dns.GetHostEntry(System.Net.Dns.GetHostName()).AddressList; //string currentIp = IPList.Where(ip=>ip.AddressFamily==System.Net.Sockets.AddressFamily.InterNetwork).Last().ToString(); string currentIp = "127.0.0.1"; if (zooKeeper.existsAsync($@"{MyApp}/{item.Key}/{apiPath}/{currentIp}:{Configuration["Port"]}") != null) zooKeeper.createAsync($@"{MyApp}/{item.Key}/{apiPath}/{currentIp}:{Configuration["Port"]}", null, Ids.OPEN_ACL_UNSAFE, CreateMode.EPHEMERAL); } } }
這里簡單介紹一下其中使用到的ZooKeeperAPI。
創建ZooKeeper的構造函數:ZooKeeper(string connectstring, int sessionTimeout, Watcher watcher, bool canBeReadOnly = false);
connectstring:ZooKeeper服務的地址和端口
sessionTimeout:連接超時時間,毫秒
watcher:觀察者,相當於一個觸發器,自己實現process方法
canBeReadOnly :是否是只讀權限
創建節點:Task<string> createAsync(string path, byte[] data, List<ACL> acl, CreateMode createMode);
path:節點路徑 必須以“/”開頭
data:節點的數據,數據大小不建議超過2M,數據格式為字節數組。
acl:權限相關
createMode:節點的類型(上篇文章講到的四種類型 持久型節點、持久有序型節點、臨時型節點、臨時有序型節點)
獲取子節點:Task<ChildrenResult> getChildrenAsync(string path, Watcher watcher);
path:節點路徑 必須以“/”開頭
watcher::觀察者,相當於一個觸發器
上面的代碼中服務的端口我沒有寫死,是通過獲取appsettings.json文件中的Port參數值設置。配置文件和Program中的代碼如下。我設置顧客服務端口為5000,訂單服務端口為5100
{ "Logging": { "LogLevel": { "Default": "Warning" } }, "Port": "5000", "AllowedHosts": "*" }
public static void Main(string[] args) { //獲取配置 var config = new ConfigurationBuilder() //需要先設置路徑 然后在路徑中找到json文件 .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile($"appsettings.json", true, true) .Build(); //設置啟動地址和端口號 CreateWebHostBuilder(args) .UseUrls("http://127.0.0.1:" + config["Port"]) .UseConfiguration(config) .Build() .Run(); }
三、啟動服務
這里介紹一個工具ZooInspector,下載地址,通過它可以很容易查看ZooKeeper里面的內容。
通過命令啟動兩個服務,通過ZooInspector看到ZooKeeper結構如下:
如果你關閉一個服務窗口,那對應的服務下面的IP列表就會消失,因為這個節點是臨時節點。
現在我們已經實現了,服務的注冊,現在可以回頭來繼續寫剛才還沒有完成的訂單調用。需改獲取訂單列表里代碼如下:
public async Task<List<Order>> GetOrders() { List<Order> orders = new List<Order>(); Order order = null; HttpClient client = new HttpClient(); for (var i = 0; i < 10; i++) { order = new Order(); order.Address = "浙江省杭州市拱墅區北部軟件園" + i; order.CustomerId = i; order.Goods = "麻辣香鍋" + i; order.Id = i; //連接ZooKeeper ZooKeeper zooKeeper = new ZooKeeper("118.24.96.212:2181", 50000, new MyWatcher()); ChildrenResult childrenResult = null; if (await zooKeeper.existsAsync("/MyApp/CustomerServices/Customer-GetCustomer") != null)
//獲取所有顧客信息服務的地址 childrenResult = await zooKeeper.getChildrenAsync("/MyApp/CustomerServices/Customer-GetCustomer"); //生成一個隨機數 Random random = new Random(); var num = random.Next(0, childrenResult.Children.Count - 1); //通過隨機數 獲取服務列表中隨機的一個地址 var url = $@"http://{childrenResult.Children[num]}/Customer/GetCustomer?Id=" + order.CustomerId;
//調用顧客服務 var result = await client.GetAsync(url); Custormer custormer = JsonConvert.DeserializeObject<Custormer>(result.Content.ReadAsStringAsync().Result); order.Custormer = custormer; orders.Add(order); } return orders; }
不過剛才我們僅僅部署了服務到一台服務器中,現在我們改變端口配置,通過命令啟動多個實例。如文章的第二個圖,顧客服務配置了3台服務器(其實都在同一電腦),訂單服務也配置了3台服務器,當訂單服務調用時,會從中隨機選一台服務器,進行調用。
通過Postman調用接口,結果中返回了訂單列表,且訂單中包含顧客信息。
本文源代碼在:ZooKeeper代碼
如果你認為文章寫的不錯,就點個推薦吧。