1. 程式人生 > >異構SOA系統架構之Asp.net實現(相容dubbo)

異構SOA系統架構之Asp.net實現(相容dubbo)

原文: 異構SOA系統架構之Asp.net實現(相容dubbo)

我們公司技術部門情況比較複雜,分到多個集團,每個集團又可能分為幾個部門,每個部門又可能分為多個小組,組織架構比較複雜,開發人員比較多。

使用的程式語言也有點複雜,主流語言有.net(C#)、Java、PHP等。

所以SOA架構需要的是異構SOA。

有的同學可能說這個簡單嗎?“把部門合併扁平化合併為一個團隊,把語言統一一種,要麼.net要麼Java。”

其實這樣的簡單粗暴並不能很好的解決問題的

首先公司組織架構就是不能隨便修改的,一個公司的組織架構就是服務於這個公司的經營理念和營銷模式,技術部門是服務機構並不直接產生價值,技術部門架構和公司組織架構高度一致能帶來業務的高效性。

其次多語言技術體系也有其可取性

      某個專案哪種語言能做的更快更好就用哪種語言

      哪種語言的程式設計師好招,就多招一些,能在各種技術方向的變化中立於不敗之地

現在繼續說SOA,說起公司對SOA選型對於.net程式設計師開始還是一件挺悲催的事情,因為公司選的是dubbo

dubbo是阿里巴巴公司開源的一個高效能優秀的服務框架,說它是個偉大的開源專案並不為過,在很多網際網路公司都有運用。

但是,dubbo是個Java專案,.net程式設計師就悲催了

為了更好的支援多語言的異構系統現狀,具體選型是dubbox+ZooKeeper+Thrift,其中Thrift是facebook開發的高效RPC,支援語言非常多, C++, Java, Python, PHP, Ruby, Erlang, Perl, Haskell, C#, Cocoa, JavaScript, Node.js, Smalltalk, and OCaml等。

有了Thrift,.net程式設計師的“春天”是不是就來了呢?

還是挺悲催,Java程式設計師幾乎不用寫額外程式碼配置一下就可以呼叫SOA服務或者釋出服務,.net程式設計師要自己維護和ZooKeeper的通訊和Thrift通訊及日誌統計和報送。

.net程式設計師苦不堪言,有些人質疑SOA選型(對.net程式設計師不公平),有些人"喊"著要.net程式設計師使用其他架構單幹 ......

後來機緣巧合,.net的SOA這個事情就落在我的身上

領導把這個任務交給我的時候,我輕鬆的說沒問題,但是時間證明這個事情比我原來想象的複雜得多,我也走了一些彎路,有過一些不太現實的想法,最終還是有了一個比較滿意的結果


一、先說ZooKeeper

1、ZooKeeper是開源專案,其原理和作用這裡不說,自行度娘

2、ZooKeeper的.net客戶端使用nuget就可以安裝使用

ZooKeeper客戶端庫也有很多開源專案支援,我這裡選的是Apache基金會的官方版本

3、本地啟動一個ZooKeeper來測試

 

ZooKeeper服務是Java開發的,ZooKeeper是個非常優秀的中介軟體,使用.net和Java呼叫區別並不大

這裡檢視ZooKeeper的工具也是Java開發的ZooInspector,正式環境我們有專門的後臺來管理,本地除錯ZooInspector就夠用了。

 

二、再說Thrift

1、Thrift也是開源專案,其原理和作用這裡不說,自行度娘

2、Thrift的.net庫使用nuget就可以安裝使用

Thrift客戶端庫也有很多開源專案支援,我這裡還是選Apache基金會的官方版本

 

三、使用.net開發一個HelloWord服務

1、按Thrift的IDL規範定義介面Thrift檔案

namespace java SOATest
namespace csharp SOATest
namespace php SOATest

service  HelloWorldService {
  string sayHello(1:string username)
}

注:Thrift規範還是自行度娘

2、使用Thrift.exe生成程式碼

Thrift.exe使用方法可以使用Thrift的help指令檢視,最好的方法還是自行度娘

3、到gen-csharp中找到剛生成的程式碼複製到專案中使用

/**
 * Autogenerated by Thrift Compiler (0.9.3)
 *
 * DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING
 *  @generated
 */
using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.IO;
using Thrift;
using Thrift.Collections;
using System.Runtime.Serialization;
using Thrift.Protocol;
using Thrift.Transport;

namespace SOATest
{
  public partial class HelloWorldService {
    public interface Iface {
      string sayHello(string username);
      #if SILVERLIGHT
      IAsyncResult Begin_sayHello(AsyncCallback callback, object state, string username);
      string End_sayHello(IAsyncResult asyncResult);
      #endif
    }

    public class Client : IDisposable, Iface {
      public Client(TProtocol prot) : this(prot, prot)
      {
      }

      public Client(TProtocol iprot, TProtocol oprot)
      {
        iprot_ = iprot;
        oprot_ = oprot;
      }

      protected TProtocol iprot_;
      protected TProtocol oprot_;
      protected int seqid_;

      public TProtocol InputProtocol
      {
        get { return iprot_; }
      }
      public TProtocol OutputProtocol
      {
        get { return oprot_; }
      }


      #region " IDisposable Support "
      private bool _IsDisposed;

      // IDisposable
      public void Dispose()
      {
        Dispose(true);
      }
      

      protected virtual void Dispose(bool disposing)
      {
        if (!_IsDisposed)
        {
          if (disposing)
          {
            if (iprot_ != null)
            {
              ((IDisposable)iprot_).Dispose();
            }
            if (oprot_ != null)
            {
              ((IDisposable)oprot_).Dispose();
            }
          }
        }
        _IsDisposed = true;
      }
      #endregion


      
      #if SILVERLIGHT
      public IAsyncResult Begin_sayHello(AsyncCallback callback, object state, string username)
      {
        return send_sayHello(callback, state, username);
      }

      public string End_sayHello(IAsyncResult asyncResult)
      {
        oprot_.Transport.EndFlush(asyncResult);
        return recv_sayHello();
      }

      #endif

      public string sayHello(string username)
      {
        #if !SILVERLIGHT
        send_sayHello(username);
        return recv_sayHello();

        #else
        var asyncResult = Begin_sayHello(null, null, username);
        return End_sayHello(asyncResult);

        #endif
      }
      #if SILVERLIGHT
      public IAsyncResult send_sayHello(AsyncCallback callback, object state, string username)
      #else
      public void send_sayHello(string username)
      #endif
      {
        oprot_.WriteMessageBegin(new TMessage("sayHello", TMessageType.Call, seqid_));
        sayHello_args args = new sayHello_args();
        args.Username = username;
        args.Write(oprot_);
        oprot_.WriteMessageEnd();
        #if SILVERLIGHT
        return oprot_.Transport.BeginFlush(callback, state);
        #else
        oprot_.Transport.Flush();
        #endif
      }

      public string recv_sayHello()
      {
        TMessage msg = iprot_.ReadMessageBegin();
        if (msg.Type == TMessageType.Exception) {
          TApplicationException x = TApplicationException.Read(iprot_);
          iprot_.ReadMessageEnd();
          throw x;
        }
        sayHello_result result = new sayHello_result();
        result.Read(iprot_);
        iprot_.ReadMessageEnd();
        if (result.__isset.success) {
          return result.Success;
        }
        throw new TApplicationException(TApplicationException.ExceptionType.MissingResult, "sayHello failed: unknown result");
      }

    }
    public class Processor : TProcessor {
      public Processor(Iface iface)
      {
        iface_ = iface;
        processMap_["sayHello"] = sayHello_Process;
      }

      protected delegate void ProcessFunction(int seqid, TProtocol iprot, TProtocol oprot);
      private Iface iface_;
      protected Dictionary<string, ProcessFunction> processMap_ = new Dictionary<string, ProcessFunction>();

      public bool Process(TProtocol iprot, TProtocol oprot)
      {
        try
        {
          TMessage msg = iprot.ReadMessageBegin();
          ProcessFunction fn;
          processMap_.TryGetValue(msg.Name, out fn);
          if (fn == null) {
            TProtocolUtil.Skip(iprot, TType.Struct);
            iprot.ReadMessageEnd();
            TApplicationException x = new TApplicationException (TApplicationException.ExceptionType.UnknownMethod, "Invalid method name: '" + msg.Name + "'");
            oprot.WriteMessageBegin(new TMessage(msg.Name, TMessageType.Exception, msg.SeqID));
            x.Write(oprot);
            oprot.WriteMessageEnd();
            oprot.Transport.Flush();
            return true;
          }
          fn(msg.SeqID, iprot, oprot);
        }
        catch (IOException)
        {
          return false;
        }
        return true;
      }

      public void sayHello_Process(int seqid, TProtocol iprot, TProtocol oprot)
      {
        sayHello_args args = new sayHello_args();
        args.Read(iprot);
        iprot.ReadMessageEnd();
        sayHello_result result = new sayHello_result();
        result.Success = iface_.sayHello(args.Username);
        oprot.WriteMessageBegin(new TMessage("sayHello", TMessageType.Reply, seqid)); 
        result.Write(oprot);
        oprot.WriteMessageEnd();
        oprot.Transport.Flush();
      }

    }


    #if !SILVERLIGHT
    [Serializable]
    #endif
    public partial class sayHello_args : TBase
    {
      private string _username;

      public string Username
      {
        get
        {
          return _username;
        }
        set
        {
          __isset.username = true;
          this._username = value;
        }
      }


      public Isset __isset;
      #if !SILVERLIGHT
      [Serializable]
      #endif
      public struct Isset {
        public bool username;
      }

      public sayHello_args() {
      }

      public void Read (TProtocol iprot)
      {
        iprot.IncrementRecursionDepth();
        try
        {
          TField field;
          iprot.ReadStructBegin();
          while (true)
          {
            field = iprot.ReadFieldBegin();
            if (field.Type == TType.Stop) { 
              break;
            }
            switch (field.ID)
            {
              case 1:
                if (field.Type == TType.String) {
                  Username = iprot.ReadString();
                } else { 
                  TProtocolUtil.Skip(iprot, field.Type);
                }
                break;
              default: 
                TProtocolUtil.Skip(iprot, field.Type);
                break;
            }
            iprot.ReadFieldEnd();
          }
          iprot.ReadStructEnd();
        }
        finally
        {
          iprot.DecrementRecursionDepth();
        }
      }

      public void Write(TProtocol oprot) {
        oprot.IncrementRecursionDepth();
        try
        {
          TStruct struc = new TStruct("sayHello_args");
          oprot.WriteStructBegin(struc);
          TField field = new TField();
          if (Username != null && __isset.username) {
            field.Name = "username";
            field.Type = TType.String;
            field.ID = 1;
            oprot.WriteFieldBegin(field);
            oprot.WriteString(Username);
            oprot.WriteFieldEnd();
          }
          oprot.WriteFieldStop();
          oprot.WriteStructEnd();
        }
        finally
        {
          oprot.DecrementRecursionDepth();
        }
      }

      public override string ToString() {
        StringBuilder __sb = new StringBuilder("sayHello_args(");
        bool __first = true;
        if (Username != null && __isset.username) {
          if(!__first) { __sb.Append(", "); }
          __first = false;
          __sb.Append("Username: ");
          __sb.Append(Username);
        }
        __sb.Append(")");
        return __sb.ToString();
      }

    }


    #if !SILVERLIGHT
    [Serializable]
    #endif
    public partial class sayHello_result : TBase
    {
      private string _success;

      public string Success
      {
        get
        {
          return _success;
        }
        set
        {
          __isset.success = true;
          this._success = value;
        }
      }


      public Isset __isset;
      #if !SILVERLIGHT
      [Serializable]
      #endif
      public struct Isset {
        public bool success;
      }

      public sayHello_result() {
      }

      public void Read (TProtocol iprot)
      {
        iprot.IncrementRecursionDepth();
        try
        {
          TField field;
          iprot.ReadStructBegin();
          while (true)
          {
            field = iprot.ReadFieldBegin();
            if (field.Type == TType.Stop) { 
              break;
            }
            switch (field.ID)
            {
              case 0:
                if (field.Type == TType.String) {
                  Success = iprot.ReadString();
                } else { 
                  TProtocolUtil.Skip(iprot, field.Type);
                }
                break;
              default: 
                TProtocolUtil.Skip(iprot, field.Type);
                break;
            }
            iprot.ReadFieldEnd();
          }
          iprot.ReadStructEnd();
        }
        finally
        {
          iprot.DecrementRecursionDepth();
        }
      }

      public void Write(TProtocol oprot) {
        oprot.IncrementRecursionDepth();
        try
        {
          TStruct struc = new TStruct("sayHello_result");
          oprot.WriteStructBegin(struc);
          TField field = new TField();

          if (this.__isset.success) {
            if (Success != null) {
              field.Name = "Success";
              field.Type = TType.String;
              field.ID = 0;
              oprot.WriteFieldBegin(field);
              oprot.WriteString(Success);
              oprot.WriteFieldEnd();
            }
          }
          oprot.WriteFieldStop();
          oprot.WriteStructEnd();
        }
        finally
        {
          oprot.DecrementRecursionDepth();
        }
      }

      public override string ToString() {
        StringBuilder __sb = new StringBuilder("sayHello_result(");
        bool __first = true;
        if (Success != null && __isset.success) {
          if(!__first) { __sb.Append(", "); }
          __first = false;
          __sb.Append("Success: ");
          __sb.Append(Success);
        }
        __sb.Append(")");
        return __sb.ToString();
      }

    }

  }
}
HelloWorldService

     注:強烈建議大家別去修改Thrift生成的程式碼

4、新建類實現生成程式碼的服務介面(實際邏輯呼叫類)

    實現介面HelloWorldService.Iface

    public class HelloWorldImp : HelloWorldService.Iface
    {
        public string sayHello(string username)
        {
            if (string.IsNullOrWhiteSpace(username))
                return null;
            string msg = string.Concat("Hello ", username);
            Console.WriteLine(msg);
            return msg;
        }
    }

5、釋出並註冊服務到ZooKeeper

    public class ServeTest
    {
        public static void Test()
        {
            ZKConsumer zooKeeper = ZKInit();
            string serviceName = "com.fang.HelloWorld$Iface";//服務名
            HelloWorldService.Iface service = new HelloWorldImp();//服務實現邏輯
            string serviceIp = "192.168.109.166";//釋出服務使用ip
            int servicePort = 5000;//釋出服務使用埠
            string group = "kg";//應用程式分組
            string serviceVersion = "1.0.0";//服務版本
            int serviceTimeOut = 5000; //服務超時閾值(單位Millisecond)
            int alertElapsed = 3000; //服務執行耗時監控報警閾值(單位Millisecond)
            int alertFailure = 10; //服務每分鐘出錯次數監控報警閾值
            //註冊併發布服務
            zooKeeper.RegistService<HelloWorldService.Iface>(serviceName, service, serviceIp, servicePort, group, serviceVersion, serviceTimeOut, alertElapsed, alertFailure);
        }
        /// <summary>
        /// 初始化zooKeeper
        /// </summary>
        /// <returns></returns>
        private static ZKConsumer ZKInit()
        {
            ZKConsumer zooKeeper = new ZKConsumer();
            zooKeeper.Connectstring = "192.168.109.166:2181";
            zooKeeper.Logger = Fang.Log.Loger.CreateDayLog("ServeTest");
            zooKeeper.Init();
            return zooKeeper;
        }
    }

注:其中ZKConsumer就是我定義的和ZooKeeper互動的類,也幾乎是.net SOA直接互動的唯一一個類,使用起來是不是非常簡單,其實實現還是比較複雜的,隨後再說

6、啟動服務看一下

6.1 其實執行時候就是開了一個socket監聽,很簡單

6.2 看一下日誌資訊

11:46:21
ZooKeeper Init
11:46:21
ZooKeeper Connect
11:46:21
ZK觸發了None事件(path:)!
11:46:21
ZooKeeper CONNECTED
11:46:21
Collecter Start
11:46:21
Consumer Subcribe:/dubbo/com.alibaba.dubbo.monitor.gen.thrift.MonitorService%24Iface/providers
11:46:22
Collecter Run
11:46:22
Collecter OnFail
11:46:27
Collecter Run
11:46:27
Collecter OnFail

以上是日誌檔案,有ZooKeeper連線資訊和訂閱日誌收集服務資訊及日誌收集資訊

日誌收集是一個執行緒排程,由於還沒有連線沒有日誌,所以Collecter都是Fail

6.3 看一下ZooKeeper的變化

ZooKeeper在dubbo節點下多出了一個節點"com.fang.HelloWorld%24Iface"及其多個子節點,其中重點是其providers子節點下有一個很長的節點,那個節點就是表示當前服務資訊的,如果服務關閉,這個資訊也會消失

都在dubbo下不難理解,因為我們選型就是dubbo,.net要相容dubbo的一些特性

 

四、做個客戶端來呼叫HelloWorld服務

1、使用Thrift.exe生成程式碼並複製到專案中

  服務端和客戶端生成程式碼是沒有區別的,這個就不再展開,需要再瞭解參考服務端生成程式碼部分

2、呼叫呼叫HelloWorld服務的原始碼

    public class HelloWorldTest
    {
        public static void Test()
        {
            ZKConsumer zooKeeper = ZKInit();
            Subcribe(zooKeeper);//訂閱com.fang.HelloWorld
            string str = null;
            do
            {
                str = Console.ReadLine();
                if (string.Equals(str, "Exit", StringComparison.CurrentCultureIgnoreCase))
                    return;
                Console.WriteLine("callDemo");
                CallService();//呼叫服務
            } while (true);
        }
        /// <summary>
        /// 訂閱AskSearchService
        /// </summary>
        /// <param name="zooKeeper"></param>
        private static void Subcribe(ZKConsumer zooKeeper)
        {
            string serviceName = "com.fang.HelloWorld$Iface";//服務名
            string serviceGroup = "kg";//服務分組
            string serviceVersion = "1.0.0.0";//服務版本
            int serviceTimeOut = 5000; //服務超時閾值(單位Millisecond)
            int alertElapsed = 3000; //服務執行耗時監控報警閾值(單位Millisecond)
            int alertFailure = 10; //服務每分鐘出錯次數監控報警閾值
            //訂閱服務
            bool state = zooKeeper.SubcribeService<HelloWorldService.Iface>(serviceName, serviceGroup, serviceVersion, serviceTimeOut, alertElapsed, alertFailure);
            Console.WriteLine(string.Concat("SubcribeService(", serviceName, ") is ", state.ToString()));
        }
        /// <summary>
        /// 初始化zooKeeper
        /// </summary>
        /// <returns></returns>
        private static ZKConsumer ZKInit()
        {
            ZKConsumer zooKeeper = new ZKConsumer();
            zooKeeper.Connectstring = "192.168.109.166:2181";
            zooKeeper.Logger = Fang.Log.Loger.CreateDayLog("HelloWorldTest");
            zooKeeper.Init();
            return zooKeeper;
        }
        /// <summary>
        /// 呼叫服務
        /// </summary>
        private static void CallService()
        {
            using (var resource = ZKConsumer.GetServiceByContainer<HelloWorldService.Iface>())
            {
                HelloWorldService.Iface service = resource.Service;
                if (service == null)
                    Console.WriteLine("service is null");
                string results = null;
                try
                {
                    results = service.sayHello("Word");
                }
                catch (Exception ex)
                {
                    Console.WriteLine(ex.Message);
                }
                if (results != null)
                    Console.WriteLine(results.ToString());
            }
        }
    }
HelloWorldTest

   注:以上看上去洋洋灑灑幾十行,貌似很複雜,其實不然

3、以上程式碼解析

 2.1 初始化和訂閱服務

  ZKInit是初始化ZooKeeper的,很簡單

  Subcribe是訂閱服務,看上去很複雜,其實就是一行程式碼,只是為了便於理解拆分寫成這樣 

  以上初始化對於每個應用程式都只需要一次
    web應用程式(站點)可以在Global.asax的Application_Start中初始化一次,也可以配置一個IHttpModule來初始化(在Init)
    控制檯和windows服務在Main方法中的開始部分初始化即可

   2.2 Test是便於測試寫了一個while迴圈,實際開發可以無視

   2.3 CallService是實際呼叫服務程式碼

    核心就是一個using及其中的GetServiceByContainer方法,及呼叫sayHello方法,其他都是安全檢測異常處理測試程式碼,算下來核心程式碼也就是兩三行

    應該說還是挺簡單的吧,當然沒有java同學用dubbo簡單,但至少這裡封裝了ZooKeeper、Thrift和日誌等。讓大家儘量少和業務無關的東西打交道

4、執行測試一下

4.1 執行之後效果如下

4.2 看一下ZooKeeper的變化

這次在consumers下增加了一個很長的節點,證明客戶端和服務端都和ZooKeeper連線上了

5、呼叫幾次試試

5.1 客戶端截圖

5.2 服務端截圖

以上測試證明是服務端和客戶端通訊沒有問題

實際上我和Java的同事也聯測了,Java呼叫.net的服務也沒有問題,.net呼叫dubbo(Java)的服務也沒有問題

另外,多個服務端和多個客戶端也是測試通過了,限於篇幅這個就不再舉例

 

五、主要原始碼解析

1、專案截圖

2、逐個解析一下

  2.1 ApplicationInfo很簡單就是讀取一些應用程式配置資訊

        AppSettings["ZooKeeperConnectstring"]是ZooKeeper連線地址

    AppSettings["ApplicationName"]是應用程式名用來程式定位及服務依賴關係圖繪製

        AppSettings["ApplicationOwner"]是專案負責人等

     2.2 Collecter收集日誌的邏輯及其執行緒排程

     2.3 Connecter用來連線ZooKeeper及維護ZooKeeper連線(重連)

     2.4 ConsumAop是客戶AOP攔截,記錄日誌到佇列

     2.5 Consumer用來客戶服務訂閱及服務路由管理

     2.6 HostStat用於服務主機資訊解析

     2.7 ISubcribe是ZooKeeper訂閱介面

     2.8 MethodReport是方法執行日誌

     2.9 Monitor是日誌定時報送作業(執行緒排程)

     2.10 Provider用於服務端啟動Socket服務及註冊到ZooKeeper

     2.11 ReportAopHandler是方法執行Aop攔截,用於服務端執行攔截,ConsumAop繼承該類

     2.12 ReportStorage日誌儲存器,並維護一個Collecter和一個Monitor執行緒排程

     2.13 ServiceConcurrent用於服務執行併發統計(客戶端及服務端)

     2.14 ServiceConfig是客戶端和服務端的公共配置

     2.15 ServiceFactor是客戶端服務工廠及Socket連線池呼叫

     2.16 ServiceHost是單個服務主機的Socket工廠及連線池

     2.17 ServiceHostManager用於服務主機叢集管理

     2.18 ServiceResource用於服務資源管理,現在用於回收Socket主機(以後可能要做成服務物件也可以回收)

     2.19 ServiceSocket,本來用於維護Socket連線,重連的,現在只是Socket包裝類

     2.20 Statistics用於日誌統計彙總

     2.21 ZKConsumer是ZooKeeper消費者,用來維護ZooKeeper連線及基於ZooKeeper的功能

     注:另外Aop、容器、資源池。執行緒排程、型別轉化等來源於面向介面主框架

     以上功能雖然可以達到.net使用和dubbo相容的服務功能,但是離dubbo在功能和穩定性上還有差距,這個建設過程需要持續下去

 

    最後,我暢想到一個夢境。一個.net小夥深情的望著Java小姑娘說,我做好準備了,我們做朋友吧。Java小姑娘點點頭。此時響起了優美的華爾茲。.net和Java手拉手在舞池裡翩翩起舞...