1. Giriş
Cloud Spanner, performanstan ve yüksek kullanılabilirlikten ödün vermeden ACID işlemleri ve SQL semantiği sağlayan, tümüyle yönetilen, yatay olarak ölçeklenebilir, küresel olarak dağıtılmış bir ilişkisel veritabanı hizmetidir.
Bu özellikler, Spanner'ı küresel bir oyuncu tabanı oluşturmak isteyen veya veri tutarlılığı konusunda endişeli olan oyunların mimarisine mükemmel bir uyum sağlar
Bu laboratuvarda, oyuncuların kaydolup oynamaya başlamalarını sağlamak için bölgesel bir Spanner veritabanıyla etkileşime giren iki Go hizmeti oluşturacaksınız.
Ardından, kaydolup oyunu oynayan oyuncuları simüle etmek için Python yük çerçevesi Locust.io'dan faydalanarak veri oluşturacaksınız. Ardından, kaç oyuncunun oynadığını belirlemek ve oyuncuların oyuna dair bazı istatistikleri belirlemek için Spanner'ı sorgularsınız oynanan maçlar ve oynanan maçlar.
Son olarak, bu laboratuvarda oluşturulan kaynakları temizleyeceksiniz.
Neler oluşturacaksınız?
Bu laboratuvar kapsamında:
- Spanner örneği oluşturma
- Herkese açık kullanıcı kaydına git alanında yazılmış bir Profil hizmetini dağıtın
- Oyuncuları oyunlara atamak, kazananları belirlemek ve oyuncuların bilgilerini güncellemek için Go'da yazılan bir Eşleştirme hizmeti dağıtın Oyun istatistikleri.
Neler öğreneceksiniz?
- Cloud Spanner örneği oluşturma
- Oyun veritabanı ve şeması nasıl oluşturulur?
- Cloud Spanner ile çalışacak Go uygulamaları dağıtma
- Locust kullanarak veri oluşturma
- Oyunlar ve oyuncularla ilgili soruları yanıtlamak için Cloud Spanner'da veri sorgulama.
İhtiyacınız olanlar
2. Kurulum ve şartlar
Proje oluşturma
Google Hesabınız (Gmail veya Google Apps) yoksa bir hesap oluşturmanız gerekir. Google Cloud Platform konsolunda ( console.cloud.google.com) oturum açın ve yeni bir proje oluşturun.
Zaten bir projeniz varsa konsolun sol üst köşesindeki proje seçimi açılan menüsünü tıklayın:
Sonra ‘YENİ PROJE’yi tıklayın. düğmesini tıklayın:
Henüz projeniz yoksa ilk projenizi oluşturmak için şuna benzer bir iletişim kutusu görmeniz gerekir:
Sonraki proje oluşturma iletişim kutusu yeni projenizin ayrıntılarını girmenize olanak tanır:
Tüm Google Cloud projeleri için benzersiz bir ad olan proje kimliğini unutmayın (yukarıdaki ad daha önce alınmış ve size uygun olmayacaktır!). Bu kod, bu codelab'in ilerleyen bölümlerinde PROJECT_ID olarak adlandırılacaktır.
Ardından, henüz yapmadıysanız Developers Console'da faturalandırmayı etkinleştirmeniz ve Google Cloud kaynaklarını kullanmanız ve Cloud Spanner API'yi etkinleştirmeniz gerekir.
Bu codelab'i çalıştırmanın maliyeti birkaç dolardan fazla değildir. Ancak daha fazla kaynak kullanmaya karar verirseniz veya bu kaynakları çalışır durumda bırakırsanız daha yüksek ücret ödemeniz gerekebilir (bu belgenin sonundaki "temizlik" bölümüne bakın). Google Cloud Spanner fiyatlandırması burada açıklanmıştır.
Yeni Google Cloud Platform kullanıcıları, bu codelab'i tamamen ücretsiz hale getirecek 300 ABD doları değerindeki ücretsiz denemeden yararlanabilir.
Google Cloud Shell Kurulumu
Google Cloud ve Spanner, dizüstü bilgisayarınızdan uzaktan çalıştırılabilse de bu codelab'de, Cloud'da çalışan bir komut satırı ortamı olan Google Cloud Shell'i kullanacağız.
Bu Debian tabanlı sanal makine, ihtiyacınız olan tüm geliştirme araçlarıyla yüklüdür. 5 GB boyutunda kalıcı bir ana dizin sunar ve Google Cloud'da çalışarak ağ performansını ve kimlik doğrulamasını büyük ölçüde iyileştirir. Yani bu codelab'de ihtiyacınız olan tek şey bir tarayıcıdır (Evet, Chromebook'ta çalışır).
- Cloud Console'dan Cloud Shell'i etkinleştirmek için Cloud Shell'i Etkinleştir'i tıklamanız yeterlidir (sağlanması ve ortama bağlanması birkaç dakika sürer).
Cloud Shell'e bağlandıktan sonra kimliğinizin doğrulanmış olduğunu ve projenin PROJECT_ID'nize ayarlanmış olduğunu göreceksiniz.
gcloud auth list
Komut çıkışı
Credentialed accounts:
- <myaccount>@<mydomain>.com (active)
gcloud config list project
Komut çıkışı
[core]
project = <PROJECT_ID>
Herhangi bir nedenle proje ayarlanmamışsa şu komutu vermeniz yeterlidir:
gcloud config set project <PROJECT_ID>
PROJECT_ID'nizi mi arıyorsunuz? Kurulum adımlarında kullandığınız kimliği kontrol edin veya Cloud Console kontrol panelinden arayın:
Cloud Shell bazı ortam değişkenlerini de varsayılan olarak ayarlar. Bu değişkenler, gelecekte komut çalıştırdığınızda işinize yarayabilir.
echo $GOOGLE_CLOUD_PROJECT
Komut çıkışı
<PROJECT_ID>
Kodu indirme
Bu laboratuvarın kodunu Cloud Shell'den indirebilirsiniz. Bu, v0.1.0 sürümüne dayalı olduğu için etiketi kontrol edin:
git clone https://github.com/cloudspannerecosystem/spanner-gaming-sample.git
cd spanner-gaming-sample/
# Check out v0.1.0 release
git checkout tags/v0.1.0 -b v0.1.0-branch
Komut çıkışı
Cloning into 'spanner-gaming-sample'...
*snip*
Switched to a new branch 'v0.1.0-branch'
Locust yükü oluşturma aracını kurun
Locust, REST API uç noktalarını test etmede kullanışlı bir Python yük testi çerçevesidir. Bu codelab'de "üretkenler"de 2 farklı yük testi var :
- authentication_server.py: Oyuncular oluşturma ve tek noktalı aramaları taklit edecek rastgele bir oyuncu edinmeyle ilgili görevleri içerir.
- match_server.py: Oyun oluşturma ve oyunları kapatmayla ilgili görevleri içerir. Oyun oluşturulduğunda şu anda oyun oynamayan 100 rastgele oyuncu atanır. Maçlar kapatıldığında games_played ve games_won istatistikleri güncellenir ve bu oyuncuların gelecekteki bir oyuna atanmasına izin verilir.
Locust'u Cloud Shell'de çalıştırmak için Python 3.7 veya üzeri gerekir. Cloud Shell'de Python 3.9 yüklü olduğu için sürümü doğrulamaktan başka bir şey yapmanız gerekmez:
python -V
Komut çıkışı
Python 3.9.12
Artık Locust gereksinimlerini yükleyebilirsiniz.
pip3 install -r requirements.txt
Komut çıkışı
Collecting locust==2.11.1
*snip*
Successfully installed ConfigArgParse-1.5.3 Flask-BasicAuth-0.2.0 Flask-Cors-3.0.10 brotli-1.0.9 gevent-21.12.0 geventhttpclient-2.0.2 greenlet-1.1.3 locust-2.11.1 msgpack-1.0.4 psutil-5.9.2 pyzmq-22.3.0 roundrobin-0.0.4 zope.event-4.5.0 zope.interface-5.4.0
Yeni yüklenen locust ikili programının bulunabilmesi için PATH yolunu güncelleyin:
PATH=~/.local/bin":$PATH"
which locust
Komut çıkışı
/home/<user>/.local/bin/locust
Özet
Bu adımda, henüz oluşturmadıysanız projenizi oluşturdunuz, Cloud Shell'i etkinleştirdiniz ve bu laboratuvarın kodunu indirdiniz.
Son olarak, laboratuvarın ilerleyen bölümlerinde yükleme oluşturma için Locust'u ayarladınız.
Sıradaki
Şimdi, Cloud Spanner örneğini ve veritabanını kuracaksınız.
3. Spanner örneği ve veritabanı oluşturma
Spanner örneğini oluşturma
Bu adımda codelab için Spanner örneğimizi oluşturduk. Soldaki Hamburger Menüsünde Spanner girişini veya "/" tuşuna basarak Spanner girişini arayın ve "Spanner" yazın.
Daha sonra, öğesini tıklayın ve örneğiniz için cloudspanner-gaming
örnek adını girip bir yapılandırma seçerek (us-central1
gibi bölgesel bir örnek seçin) ve düğüm sayısını ayarlayarak formu doldurun. Bu codelab için yalnızca 500 processing units
öğesine ihtiyacımız var.
Son olarak, "Oluştur"u tıklayın. ve birkaç saniye içinde Cloud Spanner örneğiniz olur.
Veritabanı ve şemayı oluşturma
Örneğiniz çalışmaya başladıktan sonra veritabanını oluşturabilirsiniz. Spanner, tek bir örnek üzerinde birden çok veritabanı kullanılmasına olanak sağlar.
Veritabanı, şemanızı tanımladığınız yerdir. Ayrıca, veritabanına kimlerin erişebileceğini kontrol edebilir, özel şifreleme ayarlayabilir, optimize ediciyi yapılandırabilir ve saklama süresini belirleyebilirsiniz.
Çok bölgeli örneklerde varsayılan öncüyü de yapılandırabilirsiniz. Spanner'daki veritabanları hakkında daha fazla bilgi edinin.
Bu code-lab'de, veritabanını varsayılan seçeneklerle oluşturacak ve şemayı oluşturma sırasında sağlayacaksınız.
Bu laboratuvarda iki tablo oluşturulur: oyuncular ve oyunlar.
Oyuncular zaman içinde çok sayıda oyuna katılabilir, ancak tek seferde yalnızca bir oyuna katılabilirler. Oyuncular, games_played ve games_won gibi ilgi çekici istatistikleri takip etmek için JSON veri türü olarak stats'ı da kullanır. Daha sonra başka istatistikler de eklenebileceğinden, bu oyuncular için etkili bir şekilde şemasız bir sütundur.
Oyunlar, Spanner'ın ARRAY veri türünü kullanarak katılan oyuncuları takip eder. Oyun kapatılana kadar, oyunun kazananı ve tamamlanmış özellikleri doldurulmaz.
Oyuncunun current_game oyununun geçerli bir oyun olduğundan emin olmak için bir tane yabancı anahtar vardır.
Şimdi "Veritabanı Oluştur"u tıklayarak veritabanını oluşturun inceleyebilirsiniz:
Sonra da gerekli bilgileri girin. En önemli seçenekler veritabanı adı ve diyalekttir. Bu örnekte veritabanını sample-game olarak adlandırdık ve Google Standart SQL diyalektini seçtik.
Şema için bu DDL'yi kopyalayıp kutuya yapıştırın:
CREATE TABLE games (
gameUUID STRING(36) NOT NULL,
players ARRAY<STRING(36)> NOT NULL,
winner STRING(36),
created TIMESTAMP,
finished TIMESTAMP,
) PRIMARY KEY(gameUUID);
CREATE TABLE players (
playerUUID STRING(36) NOT NULL,
player_name STRING(64) NOT NULL,
email STRING(MAX) NOT NULL,
password_hash BYTES(60) NOT NULL,
created TIMESTAMP,
updated TIMESTAMP,
stats JSON,
account_balance NUMERIC NOT NULL DEFAULT (0.00),
is_logged_in BOOL,
last_login TIMESTAMP,
valid_email BOOL,
current_game STRING(36),
FOREIGN KEY (current_game) REFERENCES games (gameUUID),
) PRIMARY KEY(playerUUID);
CREATE UNIQUE INDEX PlayerAuthentication ON players(email) STORING (password_hash);
CREATE INDEX PlayerGame ON players(current_game);
CREATE UNIQUE INDEX PlayerName ON players(player_name);
Ardından oluştur düğmesini tıklayın ve veritabanınızın oluşturulması için birkaç saniye bekleyin.
Veritabanı oluşturma sayfası şu şekilde görünmelidir:
Şimdi, kod laboratuvarının ilerleyen bölümlerinde kullanılmak üzere Cloud Shell'de bazı ortam değişkenlerini ayarlamanız gerekiyor. Bu nedenle ornek-kimligini not edin ve INSTANCE_ID ile DATABASE_ID değerlerini Cloud Shell'de ayarlayın
export SPANNER_PROJECT_ID=$GOOGLE_CLOUD_PROJECT
export SPANNER_INSTANCE_ID=cloudspanner-gaming
export SPANNER_DATABASE_ID=sample-game
Özet
Bu adımda bir Spanner örneği ve sample-game veritabanı oluşturdunuz. Bu örnek oyunda kullanılan şemayı da tanımladınız.
Sıradaki
Daha sonra, oyuncuların oyunu oynamak için kaydolabilmeleri için profil hizmetini dağıtacaksınız.
4. Profil hizmetini dağıtma
Hizmete genel bakış
Profil hizmeti, Go'da yazılmış ve gin çerçevesinden yararlanan bir REST API'dir.
Bu API'de oyuncular oyun oynamak için kaydolabilir. Bu komut, oynatıcı adını, e-posta adresini ve şifresini kabul eden basit bir POST komutuyla oluşturulur. Şifre bcrypt ile şifrelenir ve karma veritabanında depolanır.
E-posta, benzersiz bir tanımlayıcı olarak değerlendirilirken player_name, oyunun görüntülenmesi amacıyla kullanılır.
Bu API şu anda giriş işlemini desteklemiyor. Ancak bunu ek bir alıştırma olarak uygulayabilirsiniz.
Profil hizmetinin ./src/golang/profile-service/main.go dosyası, iki birincil uç noktayı aşağıdaki gibi gösterir:
func main() {
configuration, _ := config.NewConfig()
router := gin.Default()
router.SetTrustedProxies(nil)
router.Use(setSpannerConnection(configuration))
router.POST("/players", createPlayer)
router.GET("/players", getPlayerUUIDs)
router.GET("/players/:id", getPlayerByID)
router.Run(configuration.Server.URL())
}
Bu uç noktaların kodu ise oynatıcı modeline yönlendirilir.
func getPlayerByID(c *gin.Context) {
var playerUUID = c.Param("id")
ctx, client := getSpannerConnection(c)
player, err := models.GetPlayerByUUID(ctx, client, playerUUID)
if err != nil {
c.IndentedJSON(http.StatusNotFound, gin.H{"message": "player not found"})
return
}
c.IndentedJSON(http.StatusOK, player)
}
func createPlayer(c *gin.Context) {
var player models.Player
if err := c.BindJSON(&player); err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
ctx, client := getSpannerConnection(c)
err := player.AddPlayer(ctx, client)
if err != nil {
c.AbortWithError(http.StatusBadRequest, err)
return
}
c.IndentedJSON(http.StatusCreated, player.PlayerUUID)
}
Hizmetin yaptığı ilk şeylerden biri Spanner bağlantısını ayarlamak. Bu işlem, hizmet için oturum havuzunu oluşturmak amacıyla hizmet düzeyinde uygulanır.
func setSpannerConnection() gin.HandlerFunc {
ctx := context.Background()
client, err := spanner.NewClient(ctx, configuration.Spanner.URL())
if err != nil {
log.Fatal(err)
}
return func(c *gin.Context) {
c.Set("spanner_client", *client)
c.Set("spanner_context", ctx)
c.Next()
}
}
Player ve PlayerStats, aşağıdaki şekilde tanımlanan struct'lardır:
type Player struct {
PlayerUUID string `json:"playerUUID" validate:"omitempty,uuid4"`
Player_name string `json:"player_name" validate:"required_with=Password Email"`
Email string `json:"email" validate:"required_with=Player_name Password,email"`
// not stored in DB
Password string `json:"password" validate:"required_with=Player_name Email"`
// stored in DB
Password_hash []byte `json:"password_hash"`
created time.Time
updated time.Time
Stats spanner.NullJSON `json:"stats"`
Account_balance big.Rat `json:"account_balance"`
last_login time.Time
is_logged_in bool
valid_email bool
Current_game string `json:"current_game" validate:"omitempty,uuid4"`
}
type PlayerStats struct {
Games_played spanner.NullInt64 `json:"games_played"`
Games_won spanner.NullInt64 `json:"games_won"`
}
Oynatıcı ekleme işlevi, oynatıcı ekleme işlemi toplu eklemelerden ziyade tek bir ifade olduğundan ReadWrite işlemi içine bir DML ekinden yararlanır. İşlev aşağıdaki gibi görünür:
func (p *Player) AddPlayer(ctx context.Context, client spanner.Client) error {
// Validate based on struct validation rules
err := p.Validate()
if err != nil {
return err
}
// take supplied password+salt, hash. Store in user_password
passHash, err := hashPassword(p.Password)
if err != nil {
return errors.New("Unable to hash password")
}
p.Password_hash = passHash
// Generate UUIDv4
p.PlayerUUID = generateUUID()
// Initialize player stats
emptyStats := spanner.NullJSON{Value: PlayerStats{
Games_played: spanner.NullInt64{Int64: 0, Valid: true},
Games_won: spanner.NullInt64{Int64: 0, Valid: true},
}, Valid: true}
// insert into spanner
_, err = client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
stmt := spanner.Statement{
SQL: `INSERT players (playerUUID, player_name, email, password_hash, created, stats) VALUES
(@playerUUID, @playerName, @email, @passwordHash, CURRENT_TIMESTAMP(), @pStats)
`,
Params: map[string]interface{}{
"playerUUID": p.PlayerUUID,
"playerName": p.Player_name,
"email": p.Email,
"passwordHash": p.Password_hash,
"pStats": emptyStats,
},
}
_, err := txn.Update(ctx, stmt)
return err
})
if err != nil {
return err
}
// return empty error on success
return nil
}
Bir oyuncuyu UUID'sine göre almak için basit bir okuma verilir. Bu, oynatıcıya ait playerUUID, Player_name,email ve stats bilgilerini getirir.
func GetPlayerByUUID(ctx context.Context, client spanner.Client, uuid string) (Player, error) {
row, err := client.Single().ReadRow(ctx, "players",
spanner.Key{uuid}, []string{"playerUUID", "player_name", "email", "stats"})
if err != nil {
return Player{}, err
}
player := Player{}
err = row.ToStruct(&player)
if err != nil {
return Player{}, err
}
return player, nil
}
Hizmet, varsayılan olarak ortam değişkenleri kullanılarak yapılandırılır. ./src/golang/profile-service/config/config.go dosyasının ilgili bölümüne bakın.
func NewConfig() (Config, error) {
*snip*
// Server defaults
viper.SetDefault("server.host", "localhost")
viper.SetDefault("server.port", 8080)
// Bind environment variable override
viper.BindEnv("server.host", "SERVICE_HOST")
viper.BindEnv("server.port", "SERVICE_PORT")
viper.BindEnv("spanner.project_id", "SPANNER_PROJECT_ID")
viper.BindEnv("spanner.instance_id", "SPANNER_INSTANCE_ID")
viper.BindEnv("spanner.database_id", "SPANNER_DATABASE_ID")
*snip*
return c, nil
}
Varsayılan davranışın, hizmeti localhost:8080 üzerinde çalıştırmak olduğunu göreceksiniz.
Bu bilgiler doğrultusunda hizmeti çalıştırma zamanı gelmiş demektir.
Profil hizmetini çalıştırma
go komutunu kullanarak hizmeti çalıştırın. Bu işlem, bağımlılıkları indirir ve 8080 numaralı bağlantı noktasında çalışan hizmeti oluşturur:
cd ~/spanner-gaming-sample/src/golang/profile-service
go run . &
Komut çıkışı:
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /players --> main.createPlayer (4 handlers)
[GIN-debug] GET /players --> main.getPlayerUUIDs (4 handlers)
[GIN-debug] GET /players/:id --> main.getPlayerByID (4 handlers)
[GIN-debug] GET /players/:id/stats --> main.getPlayerStats (4 handlers)
[GIN-debug] Listening and serving HTTP on localhost:8080
Bir curl komutu göndererek hizmeti test edin:
curl http://localhost:8080/players \
--include \
--header "Content-Type: application/json" \
--request "POST" \
--data '{"email": "test@gmail.com","password": "s3cur3P@ss","player_name": "Test Player"}'
Komut çıkışı:
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: <date> 18:55:08 GMT
Content-Length: 38
"506a1ab6-ee5b-4882-9bb1-ef9159a72989"
Özet
Bu adımda, oyuncuların oyununuzu oynamak için kaydolmalarına olanak tanıyan profil hizmetini dağıttınız ve yeni bir oyuncu oluşturmak üzere bir POST api çağrısı göndererek hizmeti test ettiniz.
Sonraki Adımlar
Bir sonraki adımda eşleştirme hizmetini dağıtacaksınız.
5. Eşleştirme hizmetini dağıtma
Hizmete genel bakış
Eşleştirme hizmeti, Go'da yazılmış ve cin çerçevesinden yararlanan bir REST API'dir.
Bu API'de oyunlar oluşturulur ve kapatılır. Bir oyun oluşturulduğunda, o anda oyun oynamayan 10 oyuncu o oyuna atanır.
Bir oyun kapandığında, bir kazanan rastgele seçilir ve her oyuncunun games_played ve games_won istatistikleri düzenlenir. Ayrıca, her oyuncu artık oynamadığını belirtecek şekilde güncellenir ve böylece gelecekteki oyunları oynayabilir.
Eşleştirme hizmetine ait ./src/golang/matchmaking-service/main.go dosyası, profile hizmetiyle benzer bir kuruluma ve koda sahip olduğundan burada tekrarlanmaz. Bu hizmet, aşağıdaki gibi iki birincil uç noktayı sunar:
func main() {
router := gin.Default()
router.SetTrustedProxies(nil)
router.Use(setSpannerConnection())
router.POST("/games/create", createGame)
router.PUT("/games/close", closeGame)
router.Run(configuration.Server.URL())
}
Bu hizmet hem Oyun yapısı hem de daha daraltılmış Player ve PlayerStats yapılarını sağlar:
type Game struct {
GameUUID string `json:"gameUUID"`
Players []string `json:"players"`
Winner string `json:"winner"`
Created time.Time `json:"created"`
Finished spanner.NullTime `json:"finished"`
}
type Player struct {
PlayerUUID string `json:"playerUUID"`
Stats spanner.NullJSON `json:"stats"`
Current_game string `json:"current_game"`
}
type PlayerStats struct {
Games_played int `json:"games_played"`
Games_won int `json:"games_won"`
}
Eşleştirme hizmeti, oyun oluşturmak için şu anda oyun oynamayan 100 oyuncu arasından rastgele bir seçim yapar.
Büyük değişikliklerde mutasyonlar DML'den daha yüksek performanslı olduğundan, oyunu oluşturmak ve oyuncuları atamak için Spanner mutasyonları seçilir.
// Create a new game and assign players
// Players that are not currently playing a game are eligble to be selected for the new game
// Current implementation allows for less than numPlayers to be placed in a game
func (g *Game) CreateGame(ctx context.Context, client spanner.Client) error {
// Initialize game values
g.GameUUID = generateUUID()
numPlayers := 10
// Create and assign
_, err := client.ReadWriteTransaction(ctx, func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
var m []*spanner.Mutation
// get players
query := fmt.Sprintf("SELECT playerUUID FROM (SELECT playerUUID FROM players WHERE current_game IS NULL LIMIT 10000) TABLESAMPLE RESERVOIR (%d ROWS)", numPlayers)
stmt := spanner.Statement{SQL: query}
iter := txn.Query(ctx, stmt)
playerRows, err := readRows(iter)
if err != nil {
return err
}
var playerUUIDs []string
for _, row := range playerRows {
var pUUID string
if err := row.Columns(&pUUID); err != nil {
return err
}
playerUUIDs = append(playerUUIDs, pUUID)
}
// Create the game
gCols := []string{"gameUUID", "players", "created"}
m = append(m, spanner.Insert("games", gCols, []interface{}{g.GameUUID, playerUUIDs, time.Now()}))
// Update players to lock into this game
for _, p := range playerUUIDs {
pCols := []string{"playerUUID", "current_game"}
m = append(m, spanner.Update("players", pCols, []interface{}{p, g.GameUUID}))
}
txn.BufferWrite(m)
return nil
})
if err != nil {
return err
}
return nil
}
Oynatıcılar, GoogleSQL'in TABLESPACE RESERVOIR özelliği kullanılarak SQL ile rastgele seçilir.
Bir oyunu kapatmak biraz daha karmaşık bir işlemdir. Bu süreç, oyunun oyuncuları arasından rastgele bir kazananın seçilmesini, oyunun bittiği anın işaretlenmesini ve her oyuncunun güncellenmesini içerir. games_played ve games_won istatistiklerine sahip.
Bu karmaşıklık ve değişiklik miktarı nedeniyle, oyunu kapatmak için mutasyonlar tekrar seçilir.
func determineWinner(playerUUIDs []string) string {
if len(playerUUIDs) == 0 {
return ""
}
var winnerUUID string
rand.Seed(time.Now().UnixNano())
offset := rand.Intn(len(playerUUIDs))
winnerUUID = playerUUIDs[offset]
return winnerUUID
}
// Given a list of players and a winner's UUID, update players of a game
// Updating players involves closing out the game (current_game = NULL) and
// updating their game stats. Specifically, we are incrementing games_played.
// If the player is the determined winner, then their games_won stat is incremented.
func (g Game) updateGamePlayers(ctx context.Context, players []Player, txn *spanner.ReadWriteTransaction) error {
for _, p := range players {
// Modify stats
var pStats PlayerStats
json.Unmarshal([]byte(p.Stats.String()), &pStats)
pStats.Games_played = pStats.Games_played + 1
if p.PlayerUUID == g.Winner {
pStats.Games_won = pStats.Games_won + 1
}
updatedStats, _ := json.Marshal(pStats)
p.Stats.UnmarshalJSON(updatedStats)
// Update player
// If player's current game isn't the same as this game, that's an error
if p.Current_game != g.GameUUID {
errorMsg := fmt.Sprintf("Player '%s' doesn't belong to game '%s'.", p.PlayerUUID, g.GameUUID)
return errors.New(errorMsg)
}
cols := []string{"playerUUID", "current_game", "stats"}
newGame := spanner.NullString{
StringVal: "",
Valid: false,
}
txn.BufferWrite([]*spanner.Mutation{
spanner.Update("players", cols, []interface{}{p.PlayerUUID, newGame, p.Stats}),
})
}
return nil
}
// Closing game. When provided a Game, choose a random winner and close out the game.
// A game is closed by setting the winner and finished time.
// Additionally all players' game stats are updated, and the current_game is set to null to allow
// them to be chosen for a new game.
func (g *Game) CloseGame(ctx context.Context, client spanner.Client) error {
// Close game
_, err := client.ReadWriteTransaction(ctx,
func(ctx context.Context, txn *spanner.ReadWriteTransaction) error {
// Get game players
playerUUIDs, players, err := g.getGamePlayers(ctx, txn)
if err != nil {
return err
}
// Might be an issue if there are no players!
if len(playerUUIDs) == 0 {
errorMsg := fmt.Sprintf("No players found for game '%s'", g.GameUUID)
return errors.New(errorMsg)
}
// Get random winner
g.Winner = determineWinner(playerUUIDs)
// Validate game finished time is null
row, err := txn.ReadRow(ctx, "games", spanner.Key{g.GameUUID}, []string{"finished"})
if err != nil {
return err
}
if err := row.Column(0, &g.Finished); err != nil {
return err
}
// If time is not null, then the game is already marked as finished.
// That's an error.
if !g.Finished.IsNull() {
errorMsg := fmt.Sprintf("Game '%s' is already finished.", g.GameUUID)
return errors.New(errorMsg)
}
cols := []string{"gameUUID", "finished", "winner"}
txn.BufferWrite([]*spanner.Mutation{
spanner.Update("games", cols, []interface{}{g.GameUUID, time.Now(), g.Winner}),
})
// Update each player to increment stats.games_played
// (and stats.games_won if winner), and set current_game
// to null so they can be chosen for a new game
playerErr := g.updateGamePlayers(ctx, players, txn)
if playerErr != nil {
return playerErr
}
return nil
})
if err != nil {
return err
}
return nil
}
Yapılandırma, hizmetin ./src/golang/matchmaking-service/config/config.go bölümünde açıklandığı üzere tekrar ortam değişkenleri aracılığıyla işlenir.
// Server defaults
viper.SetDefault("server.host", "localhost")
viper.SetDefault("server.port", 8081)
// Bind environment variable override
viper.BindEnv("server.host", "SERVICE_HOST")
viper.BindEnv("server.port", "SERVICE_PORT")
viper.BindEnv("spanner.project_id", "SPANNER_PROJECT_ID")
viper.BindEnv("spanner.instance_id", "SPANNER_INSTANCE_ID")
viper.BindEnv("spanner.database_id", "SPANNER_DATABASE_ID")
Profil hizmetiyle çakışma olmaması için bu hizmet varsayılan olarak localhost:8081 üzerinde çalışır.
Bu bilgiler ışığında şimdi sıra eşleştirme hizmetini çalıştırmaya geldi.
Eşleştirme hizmetini çalıştırma
go komutunu kullanarak hizmeti çalıştırın. Bu şekilde hizmet, 8082 numaralı bağlantı noktasında çalışacak. Bu hizmet, profil-hizmet ile aynı bağımlılıklara sahip olduğundan yeni bağımlılıklar indirilmez.
cd ~/spanner-gaming-sample/src/golang/matchmaking-service
go run . &
Komut çıkışı:
[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.
[GIN-debug] [WARNING] Running in "debug" mode. Switch to "release" mode in production.
- using env: export GIN_MODE=release
- using code: gin.SetMode(gin.ReleaseMode)
[GIN-debug] POST /games/create --> main.createGame (4 handlers)
[GIN-debug] PUT /games/close --> main.closeGame (4 handlers)
[GIN-debug] Listening and serving HTTP on localhost:8081
Oyun oluştur
Oyun oluşturmak için hizmeti test edin. Öncelikle Cloud Shell'de yeni bir terminal açın:
Ardından, aşağıdaki curl komutunu verin:
curl http://localhost:8081/games/create \
--include \
--header "Content-Type: application/json" \
--request "POST"
Komut çıkışı:
HTTP/1.1 201 Created
Content-Type: application/json; charset=utf-8
Date: <date> 19:38:45 GMT
Content-Length: 38
"f45b0f7f-405b-4e67-a3b8-a624e990285d"
Oyunu kapat
curl http://localhost:8081/games/close \
--include \
--header "Content-Type: application/json" \
--data '{"gameUUID": "f45b0f7f-405b-4e67-a3b8-a624e990285d"}' \
--request "PUT"
Komut çıkışı:
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: <date> 19:43:58 GMT
Content-Length: 38
"506a1ab6-ee5b-4882-9bb1-ef9159a72989"
Özet
Bu adımda, oyun oluşturma ve bu oyuna oyuncu atama işlemlerini yürütmek için eşleştirme hizmetini dağıttınız. Bu hizmet ayrıca rastgele bir kazanan seçip oyundaki tüm oyuncuların oyunlarını güncelleyen bir oyunu kapatma işini de yapar. games_played ve games_won istatistiklerine sahip.
Sonraki Adımlar
Hizmetleriniz çalıştığına göre, şimdi oyuncuları kaydolup oyun oynamaya teşvik etmenin zamanı geldi.
6. Oynatmaya başlama
Profil ve eşleştirme hizmetleri çalıştığına göre, sağlanan yer işareti oluşturma araçlarını kullanarak yük oluşturabilirsiniz.
Locust, oluşturucuları çalıştırmak için bir web arayüzü sunar. Ancak bu laboratuvarda, komut satırını (–gözetimsiz seçeneği) kullanacaksınız.
Oyuncuları kaydedin
İlk olarak oyuncu oluşturmanız gerekir.
./generators/authentication_server.py dosyasında oynatıcı oluşturmak için kullanılan python kodu aşağıdaki gibi görünür:
class PlayerLoad(HttpUser):
def on_start(self):
global pUUIDs
pUUIDs = []
def generatePlayerName(self):
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=32))
def generatePassword(self):
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=32))
def generateEmail(self):
return ''.join(random.choices(string.ascii_lowercase + string.digits, k=32) + ['@'] +
random.choices(['gmail', 'yahoo', 'microsoft']) + ['.com'])
@task
def createPlayer(self):
headers = {"Content-Type": "application/json"}
data = {"player_name": self.generatePlayerName(), "email": self.generateEmail(), "password": self.generatePassword()}
with self.client.post("/players", data=json.dumps(data), headers=headers, catch_response=True) as response:
try:
pUUIDs.append(response.json())
except json.JSONDecodeError:
response.failure("Response could not be decoded as JSON")
except KeyError:
response.failure("Response did not contain expected key 'gameUUID'")
Oyuncu adları, e-posta adresleri ve şifreler rastgele oluşturulur.
Başarılı bir şekilde kaydolan oyuncular, okuma yükü oluşturmak için ikinci bir görevle alınır.
@task(5)
def getPlayer(self):
# No player UUIDs are in memory, reschedule task to run again later.
if len(pUUIDs) == 0:
raise RescheduleTask()
# Get first player in our list, removing it to avoid contention from concurrent requests
pUUID = pUUIDs[0]
del pUUIDs[0]
headers = {"Content-Type": "application/json"}
self.client.get(f"/players/{pUUID}", headers=headers, name="/players/[playerUUID]")
Aşağıdaki komut, aynı anda iki iş parçacığının eşzamanlılık (u=2) ile 30 saniye (t=30 sn) süreyle yeni oyuncular oluşturacak ./generators/authentication_server.py dosyasını çağırır:
cd ~/spanner-gaming-sample
locust -H http://127.0.0.1:8080 -f ./generators/authentication_server.py --headless -u=2 -r=2 -t=30s
Oyuncular oyunlara katılır
Kaydolan oyuncular artık oyun oynamaya başlamak istiyorlar.
./generators/match_server.py dosyasında oyun oluşturmak ve kapatmak için kullanılan python kodu aşağıdaki gibi görünür:
from locust import HttpUser, task
from locust.exception import RescheduleTask
import json
class GameMatch(HttpUser):
def on_start(self):
global openGames
openGames = []
@task(2)
def createGame(self):
headers = {"Content-Type": "application/json"}
# Create the game, then store the response in memory of list of open games.
with self.client.post("/games/create", headers=headers, catch_response=True) as response:
try:
openGames.append({"gameUUID": response.json()})
except json.JSONDecodeError:
response.failure("Response could not be decoded as JSON")
except KeyError:
response.failure("Response did not contain expected key 'gameUUID'")
@task
def closeGame(self):
# No open games are in memory, reschedule task to run again later.
if len(openGames) == 0:
raise RescheduleTask()
headers = {"Content-Type": "application/json"}
# Close the first open game in our list, removing it to avoid
# contention from concurrent requests
game = openGames[0]
del openGames[0]
data = {"gameUUID": game["gameUUID"]}
self.client.put("/games/close", data=json.dumps(data), headers=headers)
Bu oluşturma aracı çalıştırıldığında oyunları 2:1 oranında (open:close) açıp kapatır. Bu komut, oluşturma aracını 10 saniye boyunca çalıştırır (-t=10s):
locust -H http://127.0.0.1:8081 -f ./generators/match_server.py --headless -u=1 -r=1 -t=10s
Özet
Bu adımda, oyun oynamaya kaydolan oyuncuları simüle ettiniz ve ardından oyuncuların eşleştirme hizmetini kullanarak oyun oynamaları için simülasyonlar yaptınız. Bu simülasyonlar, hizmetlerimize talep göndermek için Locust Python çerçevesinden REST API
Oyuncu oluşturmak ve oyun oynamak için harcanan süreyi ve eşzamanlı kullanıcı sayısını (-u) istediğiniz gibi değiştirebilirsiniz.
Sonraki Adımlar
Simülasyondan sonra, Spanner'ı sorgulayarak çeşitli istatistikleri kontrol etmek isteyebilirsiniz.
7. Oyun istatistiklerini alma
Oyuncuların kaydolup oyun oynayabildiğini simüle ettiğimize göre artık istatistiklerinizi kontrol etmelisiniz.
Bunu yapmak için Cloud Console'u kullanarak Spanner'a sorgu istekleri gönderin.
Açık ve kapalı maçları kontrol etme
Kapalı oyun, bitti zaman damgası doldurulmuşken bitti zaman damgası NULL olarak doldurulur. Bu değer, oyun kapatıldığında ayarlanır.
Bu sorguyla yalnızca kaç oyunun açık, kaç tanesinin kapalı olduğunu kontrol edebilirsiniz:
SELECT Type, NumGames FROM
(SELECT "Open Games" as Type, count(*) as NumGames FROM games WHERE finished IS NULL
UNION ALL
SELECT "Closed Games" as Type, count(*) as NumGames FROM games WHERE finished IS NOT NULL
)
Sonuç:
|
|
|
|
|
|
Oynayan ve oynamayan oyuncu sayısını kontrol etme
Bir oyuncu, current_game sütunu ayarlanmışsa oyun oynamaktadır. Aksi takdirde, o anda oyun oynamazlar.
O nedenle, o anda oynayan ve oynamayan oyuncu sayısını karşılaştırmak için şu sorguyu kullanın:
SELECT Type, NumPlayers FROM
(SELECT "Playing" as Type, count(*) as NumPlayers FROM players WHERE current_game IS NOT NULL
UNION ALL
SELECT "Not Playing" as Type, count(*) as NumPlayers FROM players WHERE current_game IS NULL
)
Sonuç:
|
|
|
|
|
|
En iyi kazananları belirleme
Maç kapatıldığında, oyunculardan biri rastgele kazanan olarak seçilir. Söz konusu oyuncunun games_won istatistiği oyun kapanışında artar.
SELECT playerUUID, stats
FROM players
WHERE CAST(JSON_VALUE(stats, "$.games_won") AS INT64)>0
LIMIT 10;
Sonuç:
playerUUID | stats |
07e247c5-f88e-4bca-a7bc-12d2485f2f2b | {"games_played":49,"games_won":1} |
09b72595-40af-4406-a000-2fb56c58fe92 | {"games_played":56,"games_won":1} |
1002385b-02a0-462b-a8e7-05c9b27223aa | {"games_played":66,"games_won":1} |
13ec3770-7ae3-495f-9b53-6322d8e8d6c3 | {"games_played":44,"games_won":1} |
15513852-3f2a-494f-b437-fe7125d15f1b | {"games_played":49,"games_won":1} |
17faec64-4f77-475c-8df8-6ab026cf6698 | {"games_played":50,"games_won":1} |
1abfcb27-037d-446d-bb7a-b5cd17b5733d | {"games_played":63,"games_won":1} |
2109a33e-88bd-4e74-a35c-a7914d9e3bde | {"games_played":56,"games_won":2} |
222e37d9-06b0-4674-865d-a0e5fb80121e | {"games_played":60,"games_won":1} |
22ced15c-0da6-4fd9-8cb2-1ffd233b3c56 | {"games_played":50,"games_won":1} |
Özet
Bu adımda, Spanner'ı sorgulamak için Cloud Console'u kullanarak oyuncular ve oyunlarla ilgili çeşitli istatistiklerini incelediniz.
Sonraki Adımlar
Şimdi de sıra temizlik yapmaya geldi.
8. Temizleniyor (isteğe bağlı)
Temizlemek için Cloud Console'un Cloud Spanner bölümüne gidip "Cloud Spanner Örneği Oluşturma" adlı codelab adımında oluşturduğumuz "cloudspanner-gaming" örneğini silmeniz yeterlidir.
9. Tebrikler!
Tebrikler, Spanner'da başarıyla örnek bir oyun dağıttınız
Sırada ne var?
Bu laboratuvarda, golang sürücüsünü kullanarak Spanner ile çalışmayla ilgili çeşitli konulara giriş yaptınız. Bu inceleme, aşağıdakiler gibi kritik kavramları anlamanız için size daha iyi bir temel sağlayacaktır:
- Şema tasarımı
- DML ve Mutasyonlar
- Golang ile çalışma
Oyununuzun arka ucu olarak Spanner'la çalışmayla ilgili başka bir örnek için Cloud Spanner Oyun Ticareti codelab'ine göz atmayı unutmayın.