Koordinatör

webman, workerman tabanlı geliştirilmiştir, bu nedenle webman, workerman'ın kooperatif özelliklerini kullanabilir.
Koordinatör, Swoole, Swow ve Fiber olmak üzere üç farklı sürümü destekler.

Gereksinimler

  • PHP >= 8.1
  • Workerman >= 5.1.0 (composer require workerman/workerman ~v5.1)
  • webman-framework >= 2.1 (composer require workerman/webman-framework ~v2.1)
  • swoole veya swow uzantısı kurulu olmalıdır, ya da composer require revolt/event-loop (Fiber) yüklenmelidir.
  • Koordinatör varsayılan olarak kapalıdır, eventLoop'un açık şekilde ayarlanması gerekir.

Açma Yöntemi

webman, farklı süreçler için farklı sürümleri açmayı destekler, bu yüzden config/process.php dosyasında eventLoop üzerinden koordinatör sürümünü yapılandırabilirsiniz:

return [
    'webman' => [
        'handler' => Http::class,
        'listen' => 'http://0.0.0.0:8787',
        'count' => 1,
        'user' => '',
        'group' => '',
        'reusePort' => false,
        'eventLoop' => '', // Varsayılan olarak boş, Select veya Event otomatik olarak seçilir, koordinatörü açmaz
        'context' => [],
        'constructor' => [
            'requestClass' => Request::class,
            'logger' => Log::channel('default'),
            'appPath' => app_path(),
            'publicPath' => public_path()
        ]
    ],
    'my-coroutine' => [
        'handler' => Http::class,
        'listen' => 'http://0.0.0.0:8686',
        'count' => 1,
        'user' => '',
        'group' => '',
        'reusePort' => false,
        // Koordinatörü açmak için Workerman\Events\Swoole::class veya Workerman\Events\Swow::class veya Workerman\Events\Fiber::class olarak ayarlayın
        'eventLoop' => Workerman\Events\Swoole::class,
        'context' => [],
        'constructor' => [
            'requestClass' => Request::class,
            'logger' => Log::channel('default'),
            'appPath' => app_path(),
            'publicPath' => public_path()
        ]
    ]

    // ... Diğer yapılandırmalar atlandı ...
];

İpucu
webman, farklı süreçler için farklı eventLoop ayarlayabilir, bu da belirli süreçler için seçerek koordinatör açmanızı sağlar.
Örneğin, yukarıdaki yapılandırmada 8787 portundaki hizmet koordinatörü açmamışken, 8686 portundaki hizmeti açmıştır, nginx ile yönlendirme ile koordinatör ve non-koordinatör karışık olarak dağıtım yapılabilir.

Koordinatör Örneği

<?php
namespace app\controller;

use support\Response;
use Workerman\Coroutine;
use Workerman\Timer;

class IndexController
{
    public function index(): Response
    {
        Coroutine::create(function(){
            Timer::sleep(1.5);
            echo "hello coroutine\n";
        });
        return response('hello webman');
    }

}

eventLoop Swoole, Swow, Fiber olduğunda, webman her istek için bir koordinatör oluşturur, istek işlenirken yeni koordinatörler oluşturmaya devam edebilir.

Koordinatör Kısıtlamaları

  • Swoole veya Swow sürümü kullanıldığında, iş süreçleri bir blokaj IO ile karşılaştığında koordinatör otomatik olarak geçiş yapar, senkronize kodun asenkron çalışmasını sağlar.
  • Fiber sürümü kullanıldığında, blokaj IO ile karşılaşıldığında koordinatör geçiş yapmaz, süreç blokaj durumuna girebilir.
  • Koordinatör kullanırken, aynı kaynağa, örneğin veritabanı bağlantısı, dosya işlemleri vb. gibi birden çok koordinatör aynı anda işlem yapamaz; bu kaynak rekabetine neden olabilir, doğru kullanım, bağlantı havuzunu veya kilidi kullanarak kaynakları korumaktır.
  • Koordinatör kullanırken, istek ile ilgili durum verilerini küresel değişkenlerde veya statik değişkenlerde saklayamazsınız; bu, küresel veri kirliliğine neden olabilir, doğru kullanım, koordinatör bağlamı context kullanarak depolamak veya erişmektir.

Diğer Dikkat Edilmesi Gerekenler

Swow, alt kısımda PHP'nin blokaj fonksiyonlarını otomatik olarak hook'lar, ancak bu hook, PHP'nin bazı varsayılan davranışlarını etkileyebileceğinden, Swow kullanılmadığında ve Swow yüklenmiş olduğunda bug'lara neden olabilir.

Bu nedenle önerilir:

  • Projeniz Swow kullanmıyorsa, lütfen Swow uzantısını yüklemeyin.
  • Projeniz Swow kullanıyorsa, eventLoop değerini Workerman\Events\Swow::class olarak ayarlayın.

Koordinatör Bağlamı

Koordinatör ortamında, istek ile ilgili durum bilgilerini küresel değişkenlerde veya statik değişkenlerde saklamak yasaktır, çünkü bu küresel değişken kirliliğine yol açabilir, örneğin

<?php

namespace app\controller;

use support\Request;
use Workerman\Timer;

class TestController
{
    protected static $name = '';

    public function index(Request $request)
    {
        static::$name = $request->get('name');
        Timer::sleep(5);
        return static::$name;
    }
}

Dikkat
Koordinatör ortamında küresel değişkenler veya statik değişkenlerin kullanımı yasak değildir, ancak istekle ilgili durum verilerini küresel değişkenler veya statik değişkenlerde saklamak yasaktır.
Örneğin, küresel yapılandırma, veritabanı bağlantısı, bazı sınıfların tekil örnekleri gibi küresel olarak paylaşılması gereken nesne verileri küresel değişkenler veya statik değişkenler içinde saklanması önerilir.

Süreç sayısını 1 olarak ayarladığımızda, ardışık iki istek gönderdiğimizde
http://127.0.0.1:8787/test?name=lilei
http://127.0.0.1:8787/test?name=hanmeimei
Her iki isteğin dönüş değerlerini sırasıyla lilei ve hanmeimei olarak almayı bekleriz, ancak aslında her ikisi de hanmeimei dönüş değeri alır.
Bu, ikinci isteğin statik değişken $name değerini ezmesinden kaynaklanmaktadır, birinci isteğin uyku süresi sona erdiğinde dönüşte statik değişken $name zaten hanmeimei olmuştur.

Doğru yöntem ise, istek durum verilerini context içinde saklamaktır.

<?php

namespace app\controller;

use support\Request;
use support\Context;
use Workerman\Timer;

class TestController
{
    public function index(Request $request)
    {
        Context::set('name', $request->get('name'));
        Timer::sleep(5);
        return Context::get('name');
    }
}

support\Context sınıfı koordinatör bağlam verilerini depolamak için kullanılır, koordinatör işlemi tamamlandığında ilgili context verileri otomatik olarak silinir.
Koordinatör ortamında her istek ayrı bir koordinatör olduğundan, istek tamamlandığında context verileri otomatik olarak yok edilir.
Koordinatör olmayan ortamda, context istek sona erdiğinde otomatik olarak yok olacaktır.

Yerel değişkenler veri kirliliğine neden olmaz.

<?php

namespace app\controller;

use support\Request;
use support\Context;
use Workerman\Timer;

class TestController
{
    public function index(Request $request)
    {
        $name = $request->get('name');
        Timer::sleep(5);
        return $name;
    }
}

Çünkü $name yerel bir değişkendir, koordinatörler arasında yerel değişkenlere erişim yoktur, bu nedenle yerel değişkenlerin kullanımı koordinatör açısından güvenlidir.

Locker Kilidi

Bazen bazı bileşenler veya iş süreçleri koordinatör ortamını dikkate almaz, bu durumda kaynak rekabeti veya atomiklik sorunları ortaya çıkabilir, bu durumda işlem sırası sağlamak ve eşzamanlılık sorunlarını önlemek için Workerman\Locker ile kilitleme yapılabilir.

<?php

namespace app\controller;

use Redis;
use support\Response;
use Workerman\Coroutine\Locker;

class IndexController
{
    public function index(): Response
    {
        static $redis;
        if (!$redis) {
            $redis = new Redis();
            $redis->connect('127.0.0.1', 6379);
        }
        // Kilit eklenmediğinde, Swoole altında "Socket#10 has already been bound to another coroutine#10" hatasına neden olabilir
        // Swow altında coredump tetikleyebilir
        // Fiber altında Redis uzantısı senkron blokaj IO olduğu için sorun çıkmayacaktır
        Locker::lock('redis');
        $time = $redis->time();
        Locker::unlock('redis');
        return json($time);
    }

}

Paralel Eşzamanlı Çalışma

Birden fazla görevi eşzamanlı yürütmek ve sonuç almak istediğimizde, Workerman\Parallel kullanabiliriz.

<?php

namespace app\controller;

use support\Response;
use Workerman\Coroutine\Parallel;

class IndexController
{
    public function index(): Response
    {
        $parallel = new Parallel();
        for ($i=1; $i<5; $i++) {
            $parallel->add(function () use ($i) {
                // Bir şey yap
                return $i;
            });
        }
        $results = $parallel->wait();
        return json($results); // Yanıt: [1,2,3,4]
    }

}

Havuz Bağlantı Havuzu

Birden fazla koordinatör aynı bağlantıyı paylaştığında veri karışıklığına neden olabileceğinden, veritabanı, redis gibi bağlantı kaynaklarını yönetmek için bağlantı havuzu kullanılması gerekir.

webman, webman/database, webman/redis, webman/cache, webman/think-orm, webman/think-cache gibi bileşenleri sağlayarak, bu bileşenler hem koordinatör hem de koordinatör olmayan ortamda kullanılabilen bağlantı havuzunu entegre eder.

Bağlantı havuzu olmayan bir bileşeni değiştirmek isterseniz, Workerman\Pool kullanarak bunu gerçekleştirebilirsiniz, aşağıdaki kod örneğine bakınız.

Veritabanı Bileşeni

<?php
namespace app;

use Workerman\Coroutine\Context;
use Workerman\Coroutine;
use Workerman\Coroutine\Pool;

class Db
{
    private static ?Pool $pool = null;

    public static function __callStatic($name, $arguments)
    {
        if (self::$pool === null) {
            self::initializePool();
        }
        // Koordinatör bağlamından bağlantıyı al, aynı koordinatörün aynı bağlantıyı kullandığından emin ol
        $pdo = Context::get('pdo');
        if (!$pdo) {
            // Bağlantı havuzundan bağlantıyı al
            $pdo = self::$pool->get();
            Context::set('pdo', $pdo);
            // Koordinatör sonlandığında bağlantı otomatik olarak iade edilir
            Coroutine::defer(function () use ($pdo) {
                self::$pool->put($pdo);
            });
        }
        return call_user_func_array([$pdo, $name], $arguments);
    }

    private static function initializePool(): void
    {
        // Bağlantı havuzu oluştur, maksimum bağlantı sayısı 10
        self::$pool = new Pool(10);
        // Bağlantı oluşturucu ayarla (kısalık için yapılandırma dosyası okuma atlandı)
        self::$pool->setConnectionCreator(function () {
            return new \PDO('mysql:host=127.0.0.1;dbname=your_database', 'your_username', 'your_password');
        });
        // Bağlantı kapatıcı ayarla
        self::$pool->setConnectionCloser(function ($pdo) {
            $pdo = null;
        });
        // Kalp atışı kontrol cihazı ayarla
        self::$pool->setHeartbeatChecker(function ($pdo) {
            $pdo->query('SELECT 1');
        });
    }

}

Kullanım

<?php
namespace app\controller;

use support\Response;
use app\Db;

class IndexController
{
    public function index(): Response
    {
        $value = Db::query('SELECT NOW() as now')->fetchAll();
        return json($value); // [{"now":"2025-02-06 23:41:03","0":"2025-02-06 23:41:03"}]
    }

}

Daha Fazla Koordinatör ve İlgili Bileşenler Hakkında Bilgi

Daha fazla bilgi için workerman koordinatör belgelerine bakabilirsiniz.

Koordinatör ve Non-Koordinatör Karışık Dağıtım

webman, koordinatör ve non-koordinatör karışık dağılmayı destekler, örneğin non-koordinatör normal işleyişi işlerken, koordinatör yavaş IO iş akışını işler, nginx ile farklı hizmetlere yönlendirme yapılabilir.

Örneğin config/process.php

return [
    'webman' => [
        'handler' => Http::class,
        'listen' => 'http://0.0.0.0:8787',
        'count' => 1,
        'user' => '',
        'group' => '',
        'reusePort' => false,
        'eventLoop' => '', // Varsayılan olarak boş, Select veya Event otomatik olarak seçilir, koordinatörü açmaz
        'context' => [],
        'constructor' => [
            'requestClass' => Request::class,
            'logger' => Log::channel('default'),
            'appPath' => app_path(),
            'publicPath' => public_path()
        ]
    ],
    'my-coroutine' => [
        'handler' => Http::class,
        'listen' => 'http://0.0.0.0:8686',
        'count' => 1,
        'user' => '',
        'group' => '',
        'reusePort' => false,
        // Koordinatörü açmak için Workerman\Events\Swoole::class veya Workerman\Events\Swow::class veya Workerman\Events\Fiber::class olarak ayarlayın
        'eventLoop' => Workerman\Events\Swoole::class,
        'context' => [],
        'constructor' => [
            'requestClass' => Request::class,
            'logger' => Log::channel('default'),
            'appPath' => app_path(),
            'publicPath' => public_path()
        ]
    ],

    // ... Diğer yapılandırmalar atlandı ...
];

Daha sonra nginx yapılandırmasını kullanarak talepleri farklı hizmetlere yönlendirin.

upstream webman {
    server 127.0.0.1:8787;
    keepalive 10240;
}

# 8686 için yeni bir upstream ekleyin
upstream task {
   server 127.0.0.1:8686;
   keepalive 10240;
}

server {
  server_name webman.com;
  listen 80;
  access_log off;
  root /path/webman/public;

  # /tast ile başlayan talepler 8686 portuna gider, lütfen ihtiyaçlarınıza göre /tast'ı değiştirin
  location /tast {
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header Host $host;
      proxy_http_version 1.1;
      proxy_set_header Connection "";
      proxy_pass http://task;
  }

  # Diğer talepler ise orijinal 8787 portuna gider
  location / {
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header Host $host;
      proxy_http_version 1.1;
      proxy_set_header Connection "";
      if (!-f $request_filename){
          proxy_pass http://webman;
      }
  }
}