服務註冊中心之ZooKeeper系列(二) 實現一個簡單微服務之間調用的例子

  上一篇文章簡單介紹了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代碼

如果你認爲文章寫的不錯,就點個推薦吧。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章