آشنایی با Jaeger
برای پیاده سازی distributed tracing، میتوانیم از ابزار متن باز و محبوب Jaeger (با تلفظ یِگِر) که ابتدا توسط شرکت Uber منتشر شد، استفاده کنیم. نحوه کارکرد Jaeger بصورت زیر میباشد:
سادهترین روش برای راهاندازی Jager، استفاده از داکر ایمیج All in one که شامل ماژول های agent ، collector، query و ui است. پورت 6831 مربوط به agent و پورت 16686 مربوط به ui میباشد. برای جزئیات مربوط به ماژولهای مختلف از این لینک استفاده کنید.
docker run -d -p 6831:6831/udp -p 6832:6832/udp -p 14268:14268 -p 14250:14250 -p 16686:16686 -p 5778:5778 --name jaeger jaegertracing/all-in-one:latest
بعد از اجرای دستور بالا، اطلاعات مربوط به سرویسها و trace ها در ماژول Jager UI با آدرس http://localhost:16686 قابل مشاهده است.
جهت استفاده از Jaeger از پروژه تستی که شامل دو سرویس User و Gateway میباشد، استفاده میکنیم. در سرویس User، متد AddUser در صورت عدم وجود کاربر در دیتابیس، اطلاعات کاربر از گیتهاب را دریافت و در دیتابیس ذخیره میکند. سرویس Gateway از Ocelot برای مسیردهی درخواستها استفاده میکند. برای آشنایی با ocelot این پست را مطالعه نمایید.
public async Task<ApiResult<Models.User>> AddUserAsync(string username) { var result = new ApiResult<Models.User>(); var user = await _applicationDbContext.Users.FirstOrDefaultAsync(x => x.Login == username); if (user is null) { try { var url = string.Format(_appConfig.Github.ProfileUrl, username); var apiResult = await _httpClient.GetStringAsync(url); var userDto = JsonSerializer.Deserialize<UserDto>(apiResult); user = _mapper.Map<Models.User>(userDto); await _applicationDbContext.Users.AddAsync(user); await _applicationDbContext.SaveChangesAsync(); result.Result = user; result.Message = "User successfully Created"; return result; } catch (Exception e) { result.Message = "User not found"; return result; } } result.Message = "User already exist"; result.Result = user; return result; }
برای ثبت Trace مربوط به درخواستها در Jaeger ، بعد از نصب پکیجهای Jaeger و OpenTracing.Contrib.NetCore در هر دو سرویس، در کانفیگ هریک از سرویسها مورد زیر را اضافه میکنیم:
"JaegerConfig": { "Host": "localhost", "Port": 6831, "IsEnabled": true, "SamplingRate": 0.5 }
و برای اضافه شدن tracer به برنامه از متد الحاقی زیر استفاده میکنیم:
public static class Extensions { public static void AddJaeger(this IServiceCollection services, IConfiguration configuration) { var config = configuration.GetSection("JaegerConfig").Get<JaegerConfig>(); if (!(config?.IsEnabled ?? false)) return; if (string.IsNullOrEmpty(config?.Host)) throw new Exception("invalid JaegerConfig"); services.AddSingleton<ITracer>(serviceProvider => { string serviceName = Assembly.GetEntryAssembly()?.GetName().Name; ILoggerFactory loggerFactory = serviceProvider.GetRequiredService<ILoggerFactory>(); var sampler = new ProbabilisticSampler(config.SamplingRate); var reporter = new RemoteReporter.Builder() .WithLoggerFactory(loggerFactory) .WithSender(new UdpSender(config.Host, config.Port, 0)) .WithFlushInterval(TimeSpan.FromSeconds(15)) .WithMaxQueueSize(300) .Build(); ITracer tracer = new Tracer.Builder(serviceName) .WithLoggerFactory(loggerFactory) .WithSampler(sampler) .WithReporter(reporter) .Build(); GlobalTracer.Register(tracer); return tracer; }); services.AddOpenTracing(); } }
برای ثبت traceها استراتژیهای متفاوتی وجود دارد. در اینجا از ProbabilisticSampler استفاده شدهاست که در سازندهی آن میتوان درصد ثبت Traceها را مقدار دهی کرد. در نهایت این متد الحاقی را در Startup اضافه میکنیم:
builder.Services.AddJaeger(builder.Configuration);
بعد از اجرای پروژه و فراخوانی https://localhost:6000/gateway/Users/Add ، سرویس Gateway، درخواست را به سرویس User ارسال میکند و این سرویسها در Jaeger UI قابل مشاهده هستند.
جهت مشاهده trace ها ، سرویس مورد نظر را انتخاب و روی Find Traces کلیک کنید. با کلیک روی Trace مورد نظر، جزئیات فعالیت هایی مثل فراخوانی سرویس و مراجعه به دیتابیس قابل مشاهده است.
برای اضافه کردن لاگ سفارشی به یک span، میتوان از اینترفیس ITracer استفاده کرد:
private readonly IUserService _userService; private readonly ITracer _tracer; public UsersController(IUserService userService, ITracer tracer) { _userService = userService; _tracer = tracer; }
[HttpPost] public async Task<ActionResult> AddUser(AddUserDto model) { var actionName = ControllerContext.ActionDescriptor.DisplayName; using var scope = _tracer.BuildSpan(actionName).StartActive(true); scope.Span.Log($"Add user log username: {model.Username}"); return Ok(await _userService.AddUserAsync(model.Username)); }
کدهای مربوط به این مطلب در اینجا قابل دسترسی است.
PowerShell 7.x - قسمت سیزدهم - ساخت یک Static Site Generator ساده توسط PowerShell و GitHub Actions
dotnet tool update -g docfx
docfx docs/docfx.json --serve
- run: dotnet tool update -g docfx - run: docfx docs/docfx.json - name: Deploy uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: docs/_site
ایجاد کردن اولین Remote app
npm init --yes
npm install html-webpack-plugin webpack webpack-cli webpack-dev-server --save
{ "name": "remote1", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "webpack serve" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "html-webpack-plugin": "^5.3.2", "webpack": "^5.57.0", "webpack-cli": "^4.8.0", "webpack-dev-server": "^4.3.1" } }
<!DOCTYPE html> <html> <head> <title> Remote1(Localhost:7001) </title> </head> <body> <div id="dev-remote1"></div> </body> </html>
const HtmlWebpackPlugin = require('html-webpack-plugin'); const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin'); module.exports = { mode: 'development', devServer: { port: 7001, }, plugins: [ new ModuleFederationPlugin({ name: 'remote1', filename: 'remoteEntry.js', exposes: { './RemoteApp1': './src/startup', }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ], };
import('./startup');
const mount = (el) => { el.innerHTML = '<div>Remote 1 Content</div>'; }; if (process.env.NODE_ENV === 'development') { const el = document.querySelector('#dev-remote1'); if (el) { mount(el); } } export { mount };
ایجاد کردن دومین Remote app
{ "name": "remote2", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "webpack serve" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "html-webpack-plugin": "^5.3.2", "webpack": "^5.57.0", "webpack-cli": "^4.8.0", "webpack-dev-server": "^4.3.1" } }
<!DOCTYPE html> <html> <head> <title> Remote2(Localhost:7002) </title> </head> <body> <div id="dev-remote2"></div> </body> </html>
const HtmlWebpackPlugin = require('html-webpack-plugin'); const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin'); module.exports = { mode: 'development', devServer: { port: 7002, }, plugins: [ new ModuleFederationPlugin({ name: 'remote2', filename: 'remoteEntry.js', exposes: { './RemoteApp2': './src/startup', }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ], };
import('./startup');
const mount = (el) => { el.innerHTML = '<div>Remote 2 Content</div>'; }; if (process.env.NODE_ENV === 'development') { const el = document.querySelector('#dev-remote2'); if (el) { mount(el); } } export { mount };
ایجاد کردن Host app و یکپارچه کردن آن با Remote app ها
{ "name": "container", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "start": "webpack serve" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "html-webpack-plugin": "^5.3.2", "webpack": "^5.57.0", "webpack-cli": "^4.8.0", "webpack-dev-server": "^4.3.1" } }
<!DOCTYPE html> <html> <head> <title>Host App (Localhost:7000)</title> </head> <body> <div id="remote1-app"></div> <div id="remote2-app"></div> </body> </html>
const HtmlWebpackPlugin = require('html-webpack-plugin'); const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin'); module.exports = { mode: 'development', devServer: { port: 7000, }, plugins: [ new ModuleFederationPlugin({ name: 'container', remotes: { remote1: 'remote1@http://localhost:7001/remoteEntry.js', remote2: 'remote2@http://localhost:7002/remoteEntry.js', }, }), new HtmlWebpackPlugin({ template: './public/index.html', }), ], };
import('./startup');
import { mount as remote1Mount } from 'remote1/RemoteApp1'; import { mount as remote2Mount } from 'remote2/RemoteApp2'; remote1Mount(document.querySelector('#remote1-app')); remote2Mount(document.querySelector('#remote2-app'));
ملاحظات
new ModuleFederationPlugin({ name: 'remote1', filename: 'remoteEntry.js', exposes: { './RemoteApp1': './src/startup', }, shared:['react', 'react-dom'] }),
Bridge Network Driver
The bridge
networking driver is the first driver on our list. It’s simple to understand, simple to use, and simple to troubleshoot, which makes it a good networking choice for developers and those new to Docker. The bridge
driver creates a private network internal to the host so containers on this network can communicate. External access is granted by exposing ports to containers. Docker secures the network by managing rules that block connectivity between different Docker networks.
Overlay Network Driver
The built-in Docker overlay
network driver radically simplifies many of the complexities in multi-host networking. It is a swarm scope driver, which means that it operates across an entire Swarm or UCP cluster rather than individual hosts. With the overlay
driver, multi-host networks are first-class citizens inside Docker without external provisioning or components. IPAM, service discovery, multi-host connectivity, encryption, and load balancing are built right in. For control, the overlay
driver uses the encrypted Swarm control plane to manage large scale clusters at low convergence times.
MACVLAN Driver
The macvlan
driver is the newest built-in network driver and offers several unique characteristics. It’s a very lightweight driver, because rather than using any Linux bridging or port mapping, it connects container interfaces directly to host interfaces. Containers are addressed with routable IP addresses that are on the subnet of the external network.
As a result of routable IP addresses, containers communicate directly with resources that exist outside a Swarm cluster without the use of NAT and port mapping. This can aid in network visibility and troubleshooting. Additionally, the direct traffic path between containers and the host interface helps reduce latency. macvlan
is a local scope network driver which is configured per-host. As a result, there are stricter dependencies between MACVLAN and external networks, which is both a constraint and an advantage that is different from overlay
or bridge
.