انتقل إلى المحتوى الرئيسي

الفصل 2: اكتشاف الخدمة مع يوريكا

🎯 أهداف الفصل​

  • فهم مفهوم اكتشاف الخدمة
  • تكوين خادم يوريكا
  • تسجيل الخدمات كـ عملاء يوريكا
  • تنفيذ التوازن التحميلي من جانب العميل
  • إدارة التوافر العالي ليوريكا

1. لماذا اكتشاف الخدمة؟​

1.1 المشكلة بدون اكتشاف الخدمة​

المشاكل:

  • عناوين URL مضمنة في التكوين
  • إذا تغير عنوان IP → تعديل جميع العملاء
  • إذا أضفنا مثيلًا → تعديل جميع العملاء
  • التوازن التحميلي مستحيل
  • لا يوجد كشف عن الأعطال

1.2 الحل: اكتشاف الخدمة​

المزايا:

  • الخدمات تسجل ديناميكيًا
  • العملاء يكتشفون الخدمات بأسمائها
  • توازن تحميلي تلقائي
  • كشف تلقائي عن الأعطال
  • توسيع شفاف

2. نيتفليكس يوريكا​

2.1 ما هو يوريكا؟​

يوريكا هو خدمة اكتشاف طورتها نيتفليكس، تتكون من:

  • خادم يوريكا: السجل المركزي
  • عميل يوريكا: مكتبة في كل خدمة

2.2 الهندسة المعمارية​

2.3 المفاهيم الرئيسية​

المفهومالوصف
التسجيلالخدمة تسجل عند البدء
التجديد (نبض القلب)إرسال نبض كل 30 ثانية
جلب السجلالعميل يجلب قائمة الخدمات
الإلغاءالخدمة تلغي التسجيل عند التوقف
الإزالةيوريكا يزيل الخدمات بدون نبض (90 ثانية)

3. تكوين خادم يوريكا​

3.1 إنشاء المشروع​

هيكل المشروع:

eureka-server/
├── pom.xml
└── src/main/java/
└── com/example/eurekaserver/
└── EurekaServerApplication.java
└── src/main/resources/
└── application.yml

3.2 تبعيات Maven​

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.2.0</version>
<relativePath/>
</parent>

<groupId>com.example</groupId>
<artifactId>eureka-server</artifactId>
<version>1.0.0</version>
<name>eureka-server</name>

<properties>
<java.version>17</java.version>
<spring-cloud.version>2023.0.0</spring-cloud.version>
</properties>

<dependencies>
<!-- خادم يوريكا -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

<!-- الأمان (اختياري لكن موصى به) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<!-- Actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>

3.3 التطبيق الرئيسي​

package com.example.eurekaserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer // تفعيل خادم يوريكا
public class EurekaServerApplication {

public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
}
}

3.4 تكوين application.yml​

server:
port: 8761

spring:
application:
name: eureka-server

eureka:
instance:
hostname: localhost
client:
# عدم تسجيل نفسه (إنه الخادم!)
register-with-eureka: false
# عدم جلب السجل (إنه الخادم!)
fetch-registry: false
service-url:
defaultZone: http://localhost:8761/eureka/
server:
# تعطيل وضع الحماية في التطوير
enable-self-preservation: false
# فترة تنظيف المثيلات الميتة
eviction-interval-timer-in-ms: 5000

# تسجيل
logging:
level:
com.netflix.eureka: INFO
com.netflix.discovery: INFO

3.5 تأمين يوريكا (اختياري)​

package com.example.eurekaserver.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/actuator/**").permitAll()
.anyRequest().authenticated()
)
.httpBasic(basic -> {});

return http.build();
}

@Bean
public UserDetailsService userDetailsService() {
var user = User.builder()
.username("eureka")
.password(passwordEncoder().encode("eureka123"))
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(user);
}

@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

3.6 لوحة التحكم يوريكا​

بعد البدء، قم بالوصول إلى: http://localhost:8761

┌─────────────────────────────────────────────────────────────┐
│ خادم يوريكا │
│ سبرينغ كلاود │
├─────────────────────────────────────────────────────────────┤
│ حالة النظام │
│ البيئة: اختبار │
│ مركز البيانات: افتراضي │
│ الوقت الحالي: 2024-01-15 10:30:45 │
│ وقت التشغيل: 00:15:30 │
├─────────────────────────────────────────────────────────────┤
│ المثيلات المسجلة حاليًا مع يوريكا │
│ │
│ التطبيق AMIs مناطق التوفر الحالة │
│ USER-SERVICE n/a (2) (2) UP (2) │
│ PRODUCT-SERVICE n/a (1) (1) UP (1) │
│ ORDER-SERVICE n/a (1) (1) UP (1) │
└─────────────────────────────────────────────────────────────┘

4. تكوين عميل يوريكا​

4.1 تبعيات Maven (الخدمة)​

<dependencies>
<!-- سبرينغ بوت ويب -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<!-- عميل يوريكا -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

<!-- Actuator (للفحوصات الصحية) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>

<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>2023.0.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

4.2 التطبيق الرئيسي​

package com.example.userservice;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
// @EnableDiscoveryClient اختياري منذ سبرينغ كلاود 2020
// الاكتشاف تلقائي إذا كان eureka-client في classpath
public class UserServiceApplication {

public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}

4.3 تكوين العميل​

server:
port: 8081

spring:
application:
name: user-service # اسم الخدمة في يوريكا

eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
# فترة جلب السجل
registry-fetch-interval-seconds: 5
instance:
# استخدام IP بدلاً من اسم المضيف
prefer-ip-address: true
# معرف فريد للمثيل
instance-id: ${spring.application.name}:${server.port}:${random.uuid}
# فترة نبض القلب
lease-renewal-interval-in-seconds: 10
# الوقت قبل انتهاء الصلاحية (3x التجديد)
lease-expiration-duration-in-seconds: 30

# Actuator للفحوصات الصحية
management:
endpoints:
web:
exposure:
include: health,info
endpoint:
health:
show-details: always

4.4 التكوين مع الأمان​

إذا كان خادم يوريكا مؤمنًا:

eureka:
client:
service-url:
# الصيغة: http://user:password@host:port/eureka/
defaultZone: http://eureka:eureka123@localhost:8761/eureka/

5. مثال كامل: خدمة المستخدم​

5.1 هيكل المشروع​

user-service/
├── pom.xml
└── src/main/java/com/example/userservice/
├── UserServiceApplication.java
├── controller/
│ └── UserController.java
├── model/
│ └── User.java
└── service/
└── UserService.java

5.2 النموذج​

package com.example.userservice.model;

public class User {
private Long id;
private String username;
private String email;
private String firstName;
private String lastName;

// المنشئون
public User() {}

public User(Long id, String username, String email,
String firstName, String lastName) {
this.id = id;
this.username = username;
this.email = email;
this.firstName = firstName;
this.lastName = lastName;
}

// Getters و Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }

public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }

public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }

public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }

public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
}

5.3 الخدمة​

package com.example.userservice.service;

import com.example.userservice.model.User;
import org.springframework.stereotype.Service;

import jakarta.annotation.PostConstruct;
import java.util.*;

@Service
public class UserService {

private final Map<Long, User> users = new HashMap<>();

@PostConstruct
public void init() {
users.put(1L, new User(1L, "john", "john@example.com", "John", "Doe"));
users.put(2L, new User(2L, "jane", "jane@example.com", "Jane", "Smith"));
users.put(3L, new User(3L, "bob", "bob@example.com", "Bob", "Wilson"));
}

public List<User> getAllUsers() {
return new ArrayList<>(users.values());
}

public Optional<User> getUserById(Long id) {
return Optional.ofNullable(users.get(id));
}

public User createUser(User user) {
Long newId = users.keySet().stream()
.max(Long::compareTo)
.orElse(0L) + 1;
user.setId(newId);
users.put(newId, user);
return user;
}

public boolean existsById(Long id) {
return users.containsKey(id);
}
}

5.4 وحدة التحكم​

package com.example.userservice.controller;

import com.example.userservice.model.User;
import com.example.userservice.service.UserService;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/users")
public class UserController {

private final UserService userService;

@Value("${server.port}")
private String serverPort;

public UserController(UserService userService) {
this.userService = userService;
}

@GetMapping
public ResponseEntity<List<User>> getAllUsers() {
return ResponseEntity.ok(userService.getAllUsers());
}

@GetMapping("/{id}")
public ResponseEntity<User> getUserById(@PathVariable Long id) {
return userService.getUserById(id)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}

@GetMapping("/{id}/exists")
public ResponseEntity<Boolean> userExists(@PathVariable Long id) {
return ResponseEntity.ok(userService.existsById(id));
}

@PostMapping
public ResponseEntity<User> createUser(@RequestBody User user) {
return ResponseEntity.ok(userService.createUser(user));
}

// نقطة النهاية لرؤية أي مثيل يجيب
@GetMapping("/info")
public ResponseEntity<String> getInfo() {
return ResponseEntity.ok("Response from User Service on port: " + serverPort);
}
}

6. التوازن التحميلي مع Spring Cloud LoadBalancer​

6.1 التبعية​

<!-- مضمن في spring-cloud-starter-netflix-eureka-client -->
<!-- لكن يمكن إضافته بشكل صريح -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

6.2 RestTemplate مع التوازن التحميلي​

package com.example.orderservice.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {

@Bean
@LoadBalanced // تفعيل التوازن التحميلي
public RestTemplate restTemplate() {
return new RestTemplate();
}
}

6.3 الاستخدام في خدمة​

package com.example.orderservice.service;

import com.example.orderservice.model.User;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

@Service
public class UserServiceClient {

private final RestTemplate restTemplate;

public UserServiceClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}

public User getUserById(Long userId) {
// استخدام اسم الخدمة بدلاً من عنوان URL!
// Spring Cloud LoadBalancer يحل "user-service"
// إلى أحد المثيلات المسجلة في يوريكا
String url = "http://user-service/api/users/" + userId;
return restTemplate.getForObject(url, User.class);
}

public boolean userExists(Long userId) {
String url = "http://user-service/api/users/" + userId + "/exists";
return Boolean.TRUE.equals(
restTemplate.getForObject(url, Boolean.class)
);
}
}

6.4 WebClient مع التوازن التحميلي (تفاعلي)​

package com.example.orderservice.config;

import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.reactive.function.client.WebClient;

@Configuration
public class WebClientConfig {

@Bean
@LoadBalanced
public WebClient.Builder webClientBuilder() {
return WebClient.builder();
}
}
@Service
public class UserServiceClient {

private final WebClient.Builder webClientBuilder;

public UserServiceClient(WebClient.Builder webClientBuilder) {
this.webClientBuilder = webClientBuilder;
}

public Mono<User> getUserById(Long userId) {
return webClientBuilder.build()
.get()
.uri("http://user-service/api/users/" + userId)
.retrieve()
.bodyToMono(User.class);
}
}

7. خوارزميات التوازن التحميلي​

7.1 Round Robin (افتراضي)​

الطلب 1 → المثيل A
الطلب 2 → المثيل B
الطلب 3 → المثيل C
الطلب 4 → المثيل A
...

7.2 تكوين مخصص​

package com.example.orderservice.config;

import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.loadbalancer.core.RandomLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ReactorLoadBalancer;
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;

@Configuration
public class LoadBalancerConfig {

@Bean
public ReactorLoadBalancer<ServiceInstance> randomLoadBalancer(
Environment environment,
LoadBalancerClientFactory loadBalancerClientFactory) {

String name = environment.getProperty(
LoadBalancerClientFactory.PROPERTY_NAME
);

return new RandomLoadBalancer(
loadBalancerClientFactory.getLazyProvider(
name,
ServiceInstanceListSupplier.class
),
name
);
}
}

7.3 التكوين لكل خدمة​

@Configuration
@LoadBalancerClient(name = "user-service", configuration = UserServiceLBConfig.class)
public class LoadBalancerConfiguration {
}

8. التوافر العالي ليوريكا​

8.1 المشكلة: نقطة فشل واحدة​

إذا تعطل خادم يوريكا:
- لا يمكن للخدمات الجديدة التسجيل
- لا يمكن للعملاء اكتشاف الخدمات الجديدة
- لكن: يستمر التخزين المؤقت المحلي في العمل

8.2 الحل: مجموعة يوريكا​

8.3 تكوين يوريكا 1​

# eureka-server-1/application.yml
server:
port: 8761

spring:
application:
name: eureka-server
profiles:
active: peer1

eureka:
instance:
hostname: eureka1
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka2:8762/eureka/,http://eureka3:8763/eureka/

8.4 تكوين يوريكا 2​

# eureka-server-2/application.yml
server:
port: 8762

spring:
application:
name: eureka-server
profiles:
active: peer2

eureka:
instance:
hostname: eureka2
client:
register-with-eureka: true
fetch-registry: true
service-url:
defaultZone: http://eureka1:8761/eureka/,http://eureka3:8763/eureka/

8.5 تكوين العميل للمجموعة​

eureka:
client:
service-url:
# قائمة بجميع خوادم يوريكا
defaultZone: http://eureka1:8761/eureka/,http://eureka2:8762/eureka/,http://eureka3:8763/eureka/
# تجاوز الفشل التلقائي
eureka-server-connect-timeout-seconds: 5
eureka-server-read-timeout-seconds: 8

9. وضع الحفظ الذاتي​

9.1 ما هو؟​

يدخل يوريكا في وضع "الحفظ الذاتي" عندما يكتشف فقدانًا كبيرًا في نبضات القلب.

⚠️ حالة طوارئ! قد يدعي يوريكا بشكل غير صحيح أن المثيلات تعمل 
بينما هي ليست كذلك. التجديدات أقل من العتبة وبالتالي لا يتم
انتهاء صلاحية المثيلات فقط لتكون آمنة.

9.2 لماذا؟​

السيناريو: مشكلة في الشبكة بين يوريكا والخدمات
- يوريكا لم يعد يتلقى نبضات القلب
- بدون حماية: يوريكا يزيل جميع الخدمات
- النتيجة: كارثة!

مع وضع الحفظ الذاتي:
- يوريكا يكتشف الفقدان الكبير
- يحتفظ بالخدمات المسجلة
- ينتظر حتى تعود الشبكة

9.3 التكوين​

eureka:
server:
# تفعيل/تعطيل (الافتراضي: true)
enable-self-preservation: true

# عتبة التجديد (الافتراضي: 0.85 = 85%)
renewal-percent-threshold: 0.85

# فترة تحديث العتبة
renewal-threshold-update-interval-ms: 900000

# في التطوير، يمكن تعطيله
# enable-self-preservation: false

10. واجهة برمجة تطبيقات يوريكا REST​

10.1 النقاط النهائية الرئيسية​

النقطة النهائيةالطريقةالوصف
/eureka/appsGETقائمة بجميع الخدمات
/eureka/apps/\{appId\}GETتفاصيل خدمة
/eureka/apps/\{appId\}/\{instanceId\}GETتفاصيل مثيل
/eureka/apps/\{appId\}/\{instanceId\}DELETEإلغاء التسجيل
/eureka/apps/\{appId\}/\{instanceId\}/statusPUTتغيير الحالة

10.2 أمثلة​

# قائمة بجميع الخدمات
curl http://localhost:8761/eureka/apps

# الحصول على مثيلات خدمة المستخدم
curl http://localhost:8761/eureka/apps/USER-SERVICE

# التحقق من الحالة
curl http://localhost:8761/eureka/apps/USER-SERVICE/user-service:8081

# وضع خارج الخدمة (OUT_OF_SERVICE)
curl -X PUT "http://localhost:8761/eureka/apps/USER-SERVICE/user-service:8081/status?value=OUT_OF_SERVICE"

# إعادة الخدمة (UP)
curl -X PUT "http://localhost:8761/eureka/apps/USER-SERVICE/user-service:8081/status?value=UP"

10.3 استجابة XML​

<applications>
<versions__delta>1</versions__delta>
<apps__hashcode>UP_3_</apps__hashcode>
<application>
<name>USER-SERVICE</name>
<instance>
<instanceId>user-service:8081</instanceId>
<hostName>192.168.1.10</hostName>
<app>USER-SERVICE</app>
<ipAddr>192.168.1.10</ipAddr>
<status>UP</status>
<port enabled="true">8081</port>
<healthCheckUrl>http://192.168.1.10:8081/actuator/health</healthCheckUrl>
</instance>
</application>
</applications>

11. أفضل الممارسات​

11.1 اتفاقية التسمية​

# ✅ جيد
spring:
application:
name: user-service

# ❌ سيء
spring:
application:
name: UserService
name: user_service
name: users

11.2 الفحوصات الصحية​

eureka:
instance:
# تفعيل الفحوصات الصحية
health-check-url-path: /actuator/health
status-page-url-path: /actuator/info

11.3 الإيقاف التدريجي​

@Component
public class GracefulShutdown implements DisposableBean {

@Autowired
private EurekaClient eurekaClient;

@Override
public void destroy() {
// إلغاء التسجيل بشكل صحيح
eurekaClient.shutdown();
}
}

أو عبر التكوين:

server:
shutdown: graceful

spring:
lifecycle:
timeout-per-shutdown-phase: 30s

11.4 المهلات المناسبة​

eureka:
instance:
# نبض القلب كل 10 ثواني
lease-renewal-interval-in-seconds: 10
# انتهاء الصلاحية بعد 30 ثانية بدون نبض
lease-expiration-duration-in-seconds: 30
client:
# تحديث السجل كل 5 ثواني
registry-fetch-interval-seconds: 5

12. ملخص​

النقاط الرئيسية​

✅ خادم يوريكا = السجل المركزي للخدمات
✅ عميل يوريكا = يسجل ويكتشف الآخرين
✅ التوازن التحميلي = تلقائي مع @LoadBalanced
✅ الحفظ الذاتي = حماية ضد الإيجابيات الكاذبة
✅ التوافر العالي = مجموعة من خوادم يوريكا

قائمة التحقق من التكوين​

  • خادم يوريكا على المنفذ 8761
  • register-with-eureka: false للخادم
  • spring.application.name محدد لكل عميل
  • Actuator للفحوصات الصحية
  • تكوين موازن التحميل
  • مهلات مناسبة في الإنتاج

13. اختبار التحقق​

السؤال 1​

لماذا @LoadBalanced ضروري على RestTemplate؟

عرض الإجابة

@LoadBalanced يحقن معترضًا يقوم بـ:

  1. اعتراض المكالمات مع اسم الخدمة (مثل: http://user-service/...)
  2. استشارة يوريكا للحصول على المثيلات
  3. اختيار مثيل (round-robin افتراضيًا)
  4. استبدال الاسم بـ IP:port الحقيقي
  5. تنفيذ المكالمة HTTP

بدون @LoadBalanced، سيحاول RestTemplate حل "user-service" كـ DNS عادي وسيفشل.

السؤال 2​

ماذا يحدث إذا تعطل خادم يوريكا؟

عرض الإجابة

يحتفظ عملاء يوريكا بـ ذاكرة تخزين مؤقتة محلية للسجل:

  • تستمر الخدمات المعروفة بالفعل في العمل
  • لا يمكن للخدمات الجديدة التسجيل
  • لا يكتشف العملاء المثيلات الجديدة
  • النظام في وضع "متدهور" لكنه يعمل

لهذا السبب، التوافر العالي (المجموعة) مهم في الإنتاج.


الفصل التالي: التكوين المركزي →

العودة إلى الوحدة 13